@aibtc/mcp-server 1.69.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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * At Stake — the El Salvador PoX-5 prediction market and its two side legions.
3
+ *
4
+ * The market asks one bit: did any of twenty frozen El Salvador reserve scripts
5
+ * spend an output into a Stacks PoX-5 protocol bond before burn height 994,699?
6
+ * Chips are sBTC; the subject is native L1 BTC. The two are unrelated — holding
7
+ * a share is not holding the coins it is about.
8
+ *
9
+ * Complete-set escrow: 1 sat of sBTC mints 1 IDLE + 1 BONDED share, and the pair
10
+ * merges back to 1 sat any time before resolve. That is why the two prices sum
11
+ * to 1, and why `vault == idle-circ == bonded-circ` holds across every mint and
12
+ * merge.
13
+ *
14
+ * THERE IS NO ADMIN KEY AND NO ORACLE PRINCIPAL. Status is settable by exactly
15
+ * two permissionless functions: `resolve-bonded`, which carries a Bitcoin SPV
16
+ * proof that a subject output was consumed into a lockup, and `resolve-idle`,
17
+ * which anyone may call once the close height passes.
18
+ *
19
+ * On top of the market sit two legions — one per side. Voting weight IS the
20
+ * caller's share balance on that side, read live out of the market, so a legion
21
+ * is a DAO whose membership is whoever is exposed to the claim it argues. A
22
+ * passing proposal is paid in shares while the market still trades, and in
23
+ * sBTC credits after it resolves.
24
+ *
25
+ * LIVE ON STACKS MAINNET WITH REAL sBTC. `atstake_mint_complete_set` and
26
+ * `atstake_place_bid` spend sBTC, and both meter against the wallet's `sats`
27
+ * spending rail. That metering happens inside `callContract`, which reads the
28
+ * exact post-condition these calls sign in DENY mode — so it must NOT be
29
+ * repeated in the tools, or every mint would bill the rail twice.
30
+ *
31
+ * NETWORK IS DERIVED FROM THE CONTRACT ADDRESS, NOT FROM `NETWORK`.
32
+ * Every other tool in this server follows the global `NETWORK` env var. Reading
33
+ * it here would let a testnet-configured server sign market calls against a
34
+ * testnet node while quoting mainnet balances. `AT_STAKE_NETWORK` reads the
35
+ * address prefix instead, so these tools always follow the chain the contracts
36
+ * actually live on.
37
+ *
38
+ * NOT THE SAME THING AS `legion_*`. Those tools govern `aibtc-news-gov`, where
39
+ * weight is bought by contributing sBTC and proposals name a Bitcoin ordinal.
40
+ * These govern the El Salvador market's two sides, where weight is a share
41
+ * balance and proposals name a public link. Same word, different contracts.
42
+ */
43
+ import type { Network } from "./networks.js";
44
+ /** The market contract. One deployment, mainnet, no upgrade path. */
45
+ export declare const MARKET_CONTRACT = "SP5Y3W3F78NKFH4HYFNDQMJC484VZWKDH35ZR2M9.elsalvador-stakes-btc-v2";
46
+ /** The chain these contracts live on, derived rather than configured. */
47
+ export declare const AT_STAKE_NETWORK: Network;
48
+ export declare const MARKET_ADDRESS: string, MARKET_NAME: string;
49
+ /**
50
+ * sBTC, the only asset either contract moves. Declared here rather than read
51
+ * from `CONTRACTS` because the market hardcodes this exact principal — a
52
+ * post-condition naming any other token would be a post-condition on an asset
53
+ * that never moves, which in DENY mode aborts every call it guards.
54
+ */
55
+ export declare const SBTC_CONTRACT = "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token";
56
+ /** The FT asset name inside `sbtc-token`, for post-condition construction. */
57
+ export declare const SBTC_ASSET_NAME = "sbtc-token";
58
+ /**
59
+ * One side of the market: a share balance, a legion that votes with it, and the
60
+ * market status that makes it the winner.
61
+ */
62
+ export interface AtStakeSide {
63
+ /** `side` argument the market takes for this side. */
64
+ side: number;
65
+ /** Key in the market's position tuple. */
66
+ positionKey: "idle" | "bonded";
67
+ /** The legion governing this side. */
68
+ legion: string;
69
+ /** What the legion argues: "yes" pays if the coins bonded. */
70
+ argues: "yes" | "no";
71
+ /** Market status under which this side redeems 1 sat per share. */
72
+ winStatus: number;
73
+ /** Human label the contracts print in their events. */
74
+ label: string;
75
+ }
76
+ export declare const SIDE_IDLE = 0;
77
+ export declare const SIDE_BONDED = 1;
78
+ export declare const STATUS_OPEN = 0;
79
+ export declare const STATUS_BONDED = 1;
80
+ export declare const STATUS_IDLE = 2;
81
+ /** Market status code to the word the site and the contracts use. */
82
+ export declare const MARKET_STATUS: Record<number, string>;
83
+ /**
84
+ * Both sides, keyed by the word a caller is most likely to type.
85
+ *
86
+ * BONDED IS YES AND IDLE IS NO, and the mapping is not guessable from the
87
+ * numbers: `SIDE_IDLE` is u0 while `STATUS_IDLE` is u2. Every place a side is
88
+ * resolved goes through here so that off-by-one never becomes a trade on the
89
+ * side the caller did not mean.
90
+ */
91
+ export declare const SIDES: Record<"yes" | "no", AtStakeSide>;
92
+ /** Accept the share label as a synonym for the side it names. */
93
+ export declare function resolveSide(name: string): AtStakeSide;
94
+ /** The burn height after which the market can be resolved idle. */
95
+ export declare const CLOSE_HEIGHT = 994699;
96
+ /** Bids must fill at least this share of their size, in basis points. */
97
+ export declare const MIN_FILL_BPS = 100;
98
+ /** PoX-5 reward cycle indices the claim covers, inclusive. */
99
+ export declare const BOND_INDICES: number[];
100
+ /** The aggregated read the site itself renders from. */
101
+ export declare const AT_STAKE_API = "https://aibtc.com/api/legions";
102
+ export declare const MARKET_SITE_URL = "https://elsalvadorstakesbtc.com";
103
+ export declare const LEGIONS_SITE_URL = "https://aibtc.com/legions";
104
+ /** Legion `propose` and `vote` string-ascii bounds, from the contracts. */
105
+ export declare const MAX_LINK_LENGTH = 200;
106
+ export declare const MAX_TITLE_LENGTH = 128;
107
+ export declare const MAX_DESCRIPTION_LENGTH = 512;
108
+ export declare const MAX_RATIONALE_LENGTH = 256;
109
+ /** Legion proposal status codes. */
110
+ export declare const PROPOSAL_STATUS: Record<number, string>;
111
+ /**
112
+ * Market error codes, so an abort reads as a sentence instead of `u107`.
113
+ * Only the codes a caller can actually trip through these tools are listed;
114
+ * the SPV-proof range belongs to `resolve-bonded`, which this server does not
115
+ * build.
116
+ */
117
+ export declare const MARKET_ERRORS: Record<number, string>;
118
+ /** Legion error codes. */
119
+ export declare const LEGION_ERRORS: Record<number, string>;
120
+ //# sourceMappingURL=at-stake.d.ts.map
@@ -0,0 +1,183 @@
1
+ /**
2
+ * At Stake — the El Salvador PoX-5 prediction market and its two side legions.
3
+ *
4
+ * The market asks one bit: did any of twenty frozen El Salvador reserve scripts
5
+ * spend an output into a Stacks PoX-5 protocol bond before burn height 994,699?
6
+ * Chips are sBTC; the subject is native L1 BTC. The two are unrelated — holding
7
+ * a share is not holding the coins it is about.
8
+ *
9
+ * Complete-set escrow: 1 sat of sBTC mints 1 IDLE + 1 BONDED share, and the pair
10
+ * merges back to 1 sat any time before resolve. That is why the two prices sum
11
+ * to 1, and why `vault == idle-circ == bonded-circ` holds across every mint and
12
+ * merge.
13
+ *
14
+ * THERE IS NO ADMIN KEY AND NO ORACLE PRINCIPAL. Status is settable by exactly
15
+ * two permissionless functions: `resolve-bonded`, which carries a Bitcoin SPV
16
+ * proof that a subject output was consumed into a lockup, and `resolve-idle`,
17
+ * which anyone may call once the close height passes.
18
+ *
19
+ * On top of the market sit two legions — one per side. Voting weight IS the
20
+ * caller's share balance on that side, read live out of the market, so a legion
21
+ * is a DAO whose membership is whoever is exposed to the claim it argues. A
22
+ * passing proposal is paid in shares while the market still trades, and in
23
+ * sBTC credits after it resolves.
24
+ *
25
+ * LIVE ON STACKS MAINNET WITH REAL sBTC. `atstake_mint_complete_set` and
26
+ * `atstake_place_bid` spend sBTC, and both meter against the wallet's `sats`
27
+ * spending rail. That metering happens inside `callContract`, which reads the
28
+ * exact post-condition these calls sign in DENY mode — so it must NOT be
29
+ * repeated in the tools, or every mint would bill the rail twice.
30
+ *
31
+ * NETWORK IS DERIVED FROM THE CONTRACT ADDRESS, NOT FROM `NETWORK`.
32
+ * Every other tool in this server follows the global `NETWORK` env var. Reading
33
+ * it here would let a testnet-configured server sign market calls against a
34
+ * testnet node while quoting mainnet balances. `AT_STAKE_NETWORK` reads the
35
+ * address prefix instead, so these tools always follow the chain the contracts
36
+ * actually live on.
37
+ *
38
+ * NOT THE SAME THING AS `legion_*`. Those tools govern `aibtc-news-gov`, where
39
+ * weight is bought by contributing sBTC and proposals name a Bitcoin ordinal.
40
+ * These govern the El Salvador market's two sides, where weight is a share
41
+ * balance and proposals name a public link. Same word, different contracts.
42
+ */
43
+ /** The market contract. One deployment, mainnet, no upgrade path. */
44
+ export const MARKET_CONTRACT = "SP5Y3W3F78NKFH4HYFNDQMJC484VZWKDH35ZR2M9.elsalvador-stakes-btc-v2";
45
+ /** Which chain a Stacks contract id lives on, from its c32 address prefix. */
46
+ function networkFromContract(contractId) {
47
+ return contractId.startsWith("SP") || contractId.startsWith("SM")
48
+ ? "mainnet"
49
+ : "testnet";
50
+ }
51
+ /** The chain these contracts live on, derived rather than configured. */
52
+ export const AT_STAKE_NETWORK = networkFromContract(MARKET_CONTRACT);
53
+ export const [MARKET_ADDRESS, MARKET_NAME] = MARKET_CONTRACT.split(".");
54
+ /**
55
+ * sBTC, the only asset either contract moves. Declared here rather than read
56
+ * from `CONTRACTS` because the market hardcodes this exact principal — a
57
+ * post-condition naming any other token would be a post-condition on an asset
58
+ * that never moves, which in DENY mode aborts every call it guards.
59
+ */
60
+ export const SBTC_CONTRACT = "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token";
61
+ /** The FT asset name inside `sbtc-token`, for post-condition construction. */
62
+ export const SBTC_ASSET_NAME = "sbtc-token";
63
+ export const SIDE_IDLE = 0;
64
+ export const SIDE_BONDED = 1;
65
+ export const STATUS_OPEN = 0;
66
+ export const STATUS_BONDED = 1;
67
+ export const STATUS_IDLE = 2;
68
+ /** Market status code to the word the site and the contracts use. */
69
+ export const MARKET_STATUS = {
70
+ [STATUS_OPEN]: "open",
71
+ [STATUS_BONDED]: "resolved-bonded",
72
+ [STATUS_IDLE]: "resolved-idle",
73
+ };
74
+ /**
75
+ * Both sides, keyed by the word a caller is most likely to type.
76
+ *
77
+ * BONDED IS YES AND IDLE IS NO, and the mapping is not guessable from the
78
+ * numbers: `SIDE_IDLE` is u0 while `STATUS_IDLE` is u2. Every place a side is
79
+ * resolved goes through here so that off-by-one never becomes a trade on the
80
+ * side the caller did not mean.
81
+ */
82
+ export const SIDES = {
83
+ yes: {
84
+ side: SIDE_BONDED,
85
+ positionKey: "bonded",
86
+ legion: "SP5Y3W3F78NKFH4HYFNDQMJC484VZWKDH35ZR2M9.elsalvador-yes-legion-v2",
87
+ argues: "yes",
88
+ winStatus: STATUS_BONDED,
89
+ label: "bonded",
90
+ },
91
+ no: {
92
+ side: SIDE_IDLE,
93
+ positionKey: "idle",
94
+ legion: "SP5Y3W3F78NKFH4HYFNDQMJC484VZWKDH35ZR2M9.elsalvador-no-legion-v2",
95
+ argues: "no",
96
+ winStatus: STATUS_IDLE,
97
+ label: "idle",
98
+ },
99
+ };
100
+ /** Accept the share label as a synonym for the side it names. */
101
+ export function resolveSide(name) {
102
+ const key = name.trim().toLowerCase();
103
+ if (key === "yes" || key === "bonded")
104
+ return SIDES.yes;
105
+ if (key === "no" || key === "idle")
106
+ return SIDES.no;
107
+ throw new Error(`Unknown side "${name}". Use "yes" (bonded shares) or "no" (idle shares).`);
108
+ }
109
+ /** The burn height after which the market can be resolved idle. */
110
+ export const CLOSE_HEIGHT = 994699;
111
+ /** Bids must fill at least this share of their size, in basis points. */
112
+ export const MIN_FILL_BPS = 100;
113
+ /** PoX-5 reward cycle indices the claim covers, inclusive. */
114
+ export const BOND_INDICES = [2, 3, 4, 5, 6, 7];
115
+ /** The aggregated read the site itself renders from. */
116
+ export const AT_STAKE_API = "https://aibtc.com/api/legions";
117
+ export const MARKET_SITE_URL = "https://elsalvadorstakesbtc.com";
118
+ export const LEGIONS_SITE_URL = "https://aibtc.com/legions";
119
+ /** Legion `propose` and `vote` string-ascii bounds, from the contracts. */
120
+ export const MAX_LINK_LENGTH = 200;
121
+ export const MAX_TITLE_LENGTH = 128;
122
+ export const MAX_DESCRIPTION_LENGTH = 512;
123
+ export const MAX_RATIONALE_LENGTH = 256;
124
+ /** Legion proposal status codes. */
125
+ export const PROPOSAL_STATUS = {
126
+ 0: "open",
127
+ 1: "passed",
128
+ 2: "failed",
129
+ 3: "expired",
130
+ };
131
+ /**
132
+ * Market error codes, so an abort reads as a sentence instead of `u107`.
133
+ * Only the codes a caller can actually trip through these tools are listed;
134
+ * the SPV-proof range belongs to `resolve-bonded`, which this server does not
135
+ * build.
136
+ */
137
+ export const MARKET_ERRORS = {
138
+ 100: "The market is already open.",
139
+ 102: "The market is not open — it has already resolved, so shares can no longer be traded or merged.",
140
+ 103: "The close height has passed.",
141
+ 104: "The trading window is still open — the market cannot be resolved idle until burn height exceeds the close height.",
142
+ 105: "The market has not opened yet.",
143
+ 106: "Amount must be greater than zero.",
144
+ 107: "You do not hold enough shares on that side.",
145
+ 108: "The market has not resolved yet, so there is nothing to redeem.",
146
+ 109: "Unknown side — use yes/bonded or no/idle.",
147
+ 110: "You cannot transfer shares to yourself.",
148
+ 129: "You already have a resting bid on that side. Cancel it before placing another.",
149
+ 130: "You have no resting bid on that side.",
150
+ 131: "The two bids together do not cover one sat per share.",
151
+ 132: "Both sides of the match are the same buyer.",
152
+ 133: "A share pays at most one sat, so a bid above par is always a mistake.",
153
+ 206: "That principal holds no position.",
154
+ };
155
+ /** Legion error codes. */
156
+ export const LEGION_ERRORS = {
157
+ 401: "You hold fewer than the minimum shares on this side, so you cannot propose or vote.",
158
+ 404: "No proposal with that id in this legion.",
159
+ 405: "You have already voted on this proposal.",
160
+ 407: "Voting on this proposal is closed.",
161
+ 408: "Voting is still open — conclude opens when the vote window ends.",
162
+ 410: "This proposal has already been concluded.",
163
+ 417: "The payout transfer failed.",
164
+ 421: "The link cannot be empty.",
165
+ 423: "A proposer cannot vote on their own proposal.",
166
+ 432: "Too soon — the legion's global propose interval has not elapsed since the last proposal.",
167
+ 433: "The title cannot be empty.",
168
+ 434: "You already have a live proposal. Wait for it to conclude or lapse.",
169
+ 435: "The conclude window has passed — this proposal has expired and pays nobody.",
170
+ 436: "Voting has not started yet; there is a delay after propose.",
171
+ 440: "The rationale cannot be empty.",
172
+ 441: "The description cannot be empty.",
173
+ 442: "The market has closed, so this legion no longer accepts proposals.",
174
+ 443: "The vault cannot cover its outstanding credits plus this payout.",
175
+ 444: "The market has not resolved yet.",
176
+ 445: "The vault has already been redeemed.",
177
+ 446: "There are no credits to settle.",
178
+ 447: "Proposals are still live — the vault cannot be redeemed until they settle.",
179
+ 448: "The vault has not been redeemed yet, so there is nothing to claim.",
180
+ 449: "You have no unpaid credit to claim.",
181
+ 450: "You are still inside your proposer cooldown.",
182
+ };
183
+ //# sourceMappingURL=at-stake.js.map
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Chain reads for the At Stake market and its two side legions.
3
+ *
4
+ * Every read here goes to the chain the contracts live on, not the chain the
5
+ * server is configured for — see the note in `config/at-stake.ts`.
6
+ *
7
+ * Clarity values come back through `cvToJSON`, whose shape is nested and
8
+ * stringly typed. The unwrapping lives here so the tool layer reads as the
9
+ * market's own vocabulary rather than as tuple spelunking.
10
+ */
11
+ import type { ClarityValue } from "@stacks/transactions";
12
+ import type { Account } from "../transactions/builder.js";
13
+ import { type AtStakeSide } from "../config/at-stake.js";
14
+ /**
15
+ * The wallet, re-derived for the legion's chain.
16
+ *
17
+ * A testnet-configured server still holds the same private key; only the
18
+ * c32 encoding of the address differs. Re-deriving means a mainnet call is
19
+ * signed by the mainnet form of the same wallet rather than failing, or worse,
20
+ * quoting one address while signing as another.
21
+ */
22
+ export declare function getAtStakeAccount(): Promise<Account>;
23
+ /** Call a read-only function on either contract and return its JSON form. */
24
+ export declare function readContract(contractId: string, functionName: string, args?: ClarityValue[]): Promise<unknown>;
25
+ /** Read a market function. */
26
+ export declare function readMarket(functionName: string, args?: ClarityValue[]): Promise<unknown>;
27
+ /** Read a uint out of a `cvToJSON` node. */
28
+ export declare function num(cv: unknown): number;
29
+ export interface MarketSnapshot {
30
+ title: string;
31
+ opened: boolean;
32
+ status: number;
33
+ statusLabel: string;
34
+ closeHeight: number;
35
+ vault: number;
36
+ idleCirc: number;
37
+ bondedCirc: number;
38
+ createdAt: number;
39
+ }
40
+ export declare function getMarket(): Promise<MarketSnapshot>;
41
+ export interface Position {
42
+ idle: number;
43
+ bonded: number;
44
+ }
45
+ export declare function getPosition(who: string): Promise<Position>;
46
+ export interface Bid {
47
+ /** Shares the bid wants. */
48
+ amount: number;
49
+ /** sBTC escrowed against it. */
50
+ escrow: number;
51
+ }
52
+ export declare function getBid(who: string, side: AtStakeSide): Promise<Bid | null>;
53
+ /** Current burn height — the clock every window in both contracts uses. */
54
+ export declare function getBurnHeight(): Promise<number>;
55
+ /**
56
+ * Whether the market still accepts trades.
57
+ *
58
+ * Mirrors the contract's own `assert-tradeable`: open status AND the close
59
+ * height not yet passed. Derived rather than read so a caller can be told
60
+ * which of the two conditions failed.
61
+ */
62
+ export declare function isTradeable(market: MarketSnapshot, burnHeight: number): boolean;
63
+ export interface LegionParams {
64
+ minPosition: number;
65
+ payout: number;
66
+ minVoters: number;
67
+ proposerCooldown: number;
68
+ votingThreshold: number;
69
+ voteDelay: number;
70
+ voteWindow: number;
71
+ concludeWindow: number;
72
+ globalProposeInterval: number;
73
+ }
74
+ export declare function getLegionParams(side: AtStakeSide): Promise<LegionParams>;
75
+ export interface ProposeStatus {
76
+ canPropose: boolean;
77
+ eligible: boolean;
78
+ slotOpen: boolean;
79
+ cooledDown: boolean;
80
+ noLiveProposal: boolean;
81
+ potOk: boolean;
82
+ marketTradeable: boolean;
83
+ weight: number;
84
+ vault: number;
85
+ winsLeft: number;
86
+ payout: number;
87
+ votable: number;
88
+ nextProposeHeight: number;
89
+ proposerNextHeight: number;
90
+ }
91
+ export declare function getProposeStatus(side: AtStakeSide, who: string): Promise<ProposeStatus>;
92
+ export interface Proposal {
93
+ proposer: string;
94
+ payout: number;
95
+ createdAt: number;
96
+ voteEnd: number;
97
+ votableAtOpen: number;
98
+ yesWeight: number;
99
+ noWeight: number;
100
+ voterCount: number;
101
+ yesVoterCount: number;
102
+ status: number;
103
+ statusLabel: string;
104
+ reason: string;
105
+ paidInShares: boolean;
106
+ }
107
+ export declare function getProposal(side: AtStakeSide, proposalId: number): Promise<Proposal | null>;
108
+ export interface ProposalMeta {
109
+ title: string;
110
+ description: string;
111
+ link: string;
112
+ }
113
+ export declare function getProposalMeta(side: AtStakeSide, proposalId: number): Promise<ProposalMeta | null>;
114
+ /** The contract's own phase word for a proposal. */
115
+ export declare function getPhase(side: AtStakeSide, proposalId: number): Promise<string>;
116
+ export interface Settlement {
117
+ redeemed: boolean;
118
+ redeemedSats: number;
119
+ paidSats: number;
120
+ unpaidSats: number;
121
+ totalCredits: number;
122
+ won: boolean;
123
+ }
124
+ export declare function getSettlement(side: AtStakeSide): Promise<Settlement>;
125
+ /**
126
+ * Predict what `conclude` will decide, in the contract's own decision order.
127
+ *
128
+ * The chain decides for real at the block that mines the transaction; this is
129
+ * the tally as of now, so a caller can see that they are about to burn gas on
130
+ * a proposal that fails for want of one more voter.
131
+ */
132
+ export declare function predictOutcome(proposal: Proposal, params: LegionParams, proposerStillHolding: boolean, vault: number, totalCredits: number, tradeable: boolean): {
133
+ outcome: "passed" | "failed";
134
+ reason: string;
135
+ payout: number;
136
+ };
137
+ /**
138
+ * Turn a contract abort into the sentence behind its code.
139
+ *
140
+ * The two contracts use disjoint ranges — the market is 100-313, the legions
141
+ * 401-450 — so one lookup across both tables cannot collide.
142
+ */
143
+ export declare function explainAbort(error: unknown): Error;
144
+ //# sourceMappingURL=at-stake.service.d.ts.map