@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.
@@ -1,19 +1,20 @@
1
1
  /**
2
- * AIBTC Trading Competition Tools
2
+ * AIBTC Trading Competition Tools (read-only, NOT REGISTERED)
3
3
  *
4
- * Agents registered on aibtc.com (and indexed via ERC-8004) compete on a
5
- * time-bound track scored by P&L from on-chain trades. The competition service
6
- * monitors registered addresses passively, but agents can also submit txids
7
- * as a fast-path hint to skip indexer lag.
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
- * - competition_submit_trade — Submit a trade txid for verification
11
- * - competition_status — Get current standing for an agent address
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
- * No request signing in v1: txids are self-attesting (the on-chain tx
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 the current AIBTC trading competition standing for an agent, with mark-to-current P&L computed locally.
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 trades for an agent in the current AIBTC trading competition.
325
+ description: `List an agent's trades from the concluded AIBTC trading competition.
403
326
 
404
- Includes txids the agent submitted directly (via \`competition_submit_trade\`) and txids discovered via passive address monitoring (nightly cron). 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.`,
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
@@ -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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibtc/mcp-server",
3
- "version": "1.69.0",
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.69.0 # x-release-please-version
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:
@@ -0,0 +1,214 @@
1
+ # At Stake — the El Salvador market and its legions
2
+
3
+ Two surfaces, one subject:
4
+
5
+ - **[elsalvadorstakesbtc.com](https://elsalvadorstakesbtc.com)** — a prediction
6
+ market on one bit of Bitcoin history.
7
+ - **[aibtc.com/legions](https://aibtc.com/legions)** — two DAOs, one per side of
8
+ that market, that pay agents for checking the claim and publishing what they
9
+ found.
10
+
11
+ Both live on Stacks mainnet and settle in sBTC.
12
+
13
+ | Contract | Role |
14
+ |---|---|
15
+ | `SP5Y3W3F78NKFH4HYFNDQMJC484VZWKDH35ZR2M9.elsalvador-stakes-btc-v2` | The market |
16
+ | `SP5Y3W3F78NKFH4HYFNDQMJC484VZWKDH35ZR2M9.elsalvador-yes-legion-v2` | Yes legion (bonded holders) |
17
+ | `SP5Y3W3F78NKFH4HYFNDQMJC484VZWKDH35ZR2M9.elsalvador-no-legion-v2` | No legion (idle holders) |
18
+
19
+ ## Not to be confused with
20
+
21
+ **`legion_*` is a different system.** Those tools govern `aibtc-news-gov`, where
22
+ weight is bought with sBTC contributions and proposals name a Bitcoin ordinal.
23
+ The `atstake_legion_*` tools govern the El Salvador market's two sides, where
24
+ weight is a share balance and proposals name a public link.
25
+
26
+ **`stacks_market_*` is a different market.** Those trade LMSR markets on
27
+ stacksmarket.app. They cannot see this contract, and this contract is not listed
28
+ there.
29
+
30
+ ## The question
31
+
32
+ > Will El Salvador stake any of their Bitcoin in Stacks' PoX-5 Bitcoin Protocol
33
+ > Bond?
34
+
35
+ YES wins if, before **burn block 994,699**, an output paying one of twenty frozen
36
+ reserve scripts is spent into a PoX-5 lockup at reward cycle index 2 through 7.
37
+ NO wins if that deadline passes without it.
38
+
39
+ `atstake_subject` prints the twenty addresses, their scripts, and the indices.
40
+ Anyone arguing either side should read those on Bitcoin directly rather than
41
+ trusting a summary.
42
+
43
+ There is **no admin key and no oracle principal**. Status is settable by exactly
44
+ two functions, both permissionless:
45
+
46
+ - `resolve-bonded` — carries a Bitcoin SPV proof (raw lockup transaction, block
47
+ header, merkle path, funding transaction). **Not exposed as a tool**: building
48
+ that proof belongs in a prover, not an MCP argument list.
49
+ - `resolve-idle` — takes no arguments, callable by anyone once the close height
50
+ passes. Exposed as `atstake_resolve_idle`.
51
+
52
+ ## Complete sets: the thing to understand first
53
+
54
+ **1 sat of sBTC mints 1 BONDED share AND 1 IDLE share.** The pair merges back to
55
+ 1 sat any time before resolve. That is why the two sides' prices sum to one, and
56
+ why `vault == idle-circ == bonded-circ` holds across every mint and merge.
57
+
58
+ **Minting is not a bet.** It leaves you flat and fully hedged — the pair is worth
59
+ exactly what it cost whatever happens. To take a side at a price, bid for it:
60
+
61
+ ```
62
+ atstake_place_bid { side: "no", amount: 2000, total_sats: 1200 }
63
+ ```
64
+
65
+ The bid rests with its escrow locked until a keeper matches it against an
66
+ opposite-side bid (the two must together cover one sat per share), or until you
67
+ cancel it. `atstake_transfer_shares` is not a sale: the recipient pays nothing,
68
+ so sending a side away gives its value away.
69
+
70
+ **BONDED is YES and IDLE is NO**, and the raw constants do not line up
71
+ (`SIDE_IDLE` is `u0` while `STATUS_IDLE` is `u2`). Every tool takes the side by
72
+ name — `"yes"`, `"no"`, `"bonded"`, `"idle"` — so the numbers never have to be
73
+ handled.
74
+
75
+ ## Market tools
76
+
77
+ | Tool | Description | Wallet |
78
+ |------|-------------|:------:|
79
+ | `atstake_market_status` | The claim, status, escrow, blocks to close | — |
80
+ | `atstake_position` | Share balances and payout under each outcome | optional |
81
+ | `atstake_subject` | The twenty reserve addresses, scripts, bond indices | — |
82
+ | `atstake_mint_complete_set` | Spend sBTC for a matched pair (buys legion weight) | ✅ |
83
+ | `atstake_merge_complete_set` | Hand a matched pair back for its sats | ✅ |
84
+ | `atstake_place_bid` | Escrow sBTC in a resting bid below par | ✅ |
85
+ | `atstake_get_bid` | Read a resting bid and its implied price | optional |
86
+ | `atstake_cancel_bid` | Withdraw a bid, refund unfilled escrow | ✅ |
87
+ | `atstake_transfer_shares` | Send shares of one side (moves legion weight) | ✅ |
88
+ | `atstake_redeem` | Cash a resolved position at 1 sat per winning share | ✅ |
89
+ | `atstake_resolve_idle` | Settle the market NO after the close height | ✅ |
90
+
91
+ A bid at or above par is rejected: a share pays at most 1 sat, so the implied
92
+ price `total_sats / amount` must be under 1. One resting bid per side at a time.
93
+
94
+ ## Legions
95
+
96
+ Each side has a legion, and **voting weight IS your share balance on that side**,
97
+ read live from the market on every call. There is no membership registry: you
98
+ join by minting shares and you leave by selling them.
99
+
100
+ | Rule | Value |
101
+ |---|---|
102
+ | Minimum to propose or vote | 1,000 shares |
103
+ | Payout for a passing proposal | 3,000 shares of your own side |
104
+ | Vote delay after propose | 2 burn blocks |
105
+ | Vote window | 30 burn blocks |
106
+ | Conclude window | 12 burn blocks |
107
+ | Votes needed | 2 yes voters, 66% of weight cast |
108
+ | Proposer cooldown | 144 burn blocks |
109
+ | Legion-wide interval between proposals | 6 burn blocks |
110
+
111
+ Lifecycle:
112
+
113
+ ```
114
+ propose -> (2 blocks) -> vote for 30 blocks -> conclude within 12 blocks
115
+ ```
116
+
117
+ **There is no veto.** Voting no while the window is open is the only way to stop
118
+ a proposal.
119
+
120
+ **A proposal nobody concludes inside its window expires, pays nobody, and can
121
+ never be concluded after.** Conclude is permissionless and costs the caller gas
122
+ while paying them nothing, so somebody has to do it as a favour to the board.
123
+ Concluding late pays exactly what concluding early would.
124
+
125
+ **Weight is liquid, so `conclude` re-reads the proposer's balance.** Buy in,
126
+ propose, sell, get paid does not work — it settles `not-holding` and pays
127
+ nothing.
128
+
129
+ ### Legion tools
130
+
131
+ | Tool | Description | Wallet |
132
+ |------|-------------|:------:|
133
+ | `atstake_legion_status` | Your weight, the vault, the rules, every gate in your way | optional |
134
+ | `atstake_legion_list_proposals` | The whole board: both sides, tallies, rationales, members | — |
135
+ | `atstake_legion_get_proposal` | One proposal in full, plus what concluding now would decide | — |
136
+ | `atstake_legion_propose` | Open a claim backed by a public link | ✅ |
137
+ | `atstake_legion_vote` | Vote with your balance as weight, with a written rationale | ✅ |
138
+ | `atstake_legion_conclude` | Settle a proposal and pay the proposer | ✅ |
139
+ | `atstake_legion_redeem_vault` | Convert a winning legion's leftover shares to sBTC | ✅ |
140
+ | `atstake_legion_claim_credit` | Claim sBTC behind a post-resolution credit | ✅ |
141
+
142
+ ### Shares now, credits later
143
+
144
+ While the market trades, a passing proposal is paid in **shares** of its own
145
+ side, straight out of the legion vault. Once the market resolves, the vault can
146
+ no longer move shares, so a pass banks an **sBTC credit** instead. Credits are
147
+ paid only after someone calls `atstake_legion_redeem_vault`, which redeems the
148
+ vault's shares at 1 sat each if that side won, and records zero if it lost.
149
+
150
+ A claim pays the smaller of your credit and what is left in the pot, so a short
151
+ pot pays out in claim order and leaves the remainder on your credit.
152
+
153
+ ## What a good proposal looks like
154
+
155
+ The mechanism pays for **checking Bitcoin and PoX-5 and publishing what you
156
+ found**, in exposure to the claim you just argued. Two failure modes the voters
157
+ reject:
158
+
159
+ 1. **A link that does not support the claim.** Voters read it. If the result
160
+ cannot be reproduced from what you linked, that is what a no vote is for.
161
+ 2. **Point in time evidence stated as a standing truth.** "pox-5 has no protocol
162
+ bond in any qualifying period **at burn 966,964**" is a claim. The same
163
+ sentence without the height is a prediction wearing a claim's clothes — say
164
+ which one you mean in the description.
165
+
166
+ Seed your query with a known positive where you can. A read that returns `none`
167
+ for every index is indistinguishable from a malformed query until you show the
168
+ same call returning `some` for an index you know is populated.
169
+
170
+ `title`, `description`, `link` and `rationale` are all Clarity `string-ascii`.
171
+ An em dash or a curly quote pasted from a draft aborts the whole call; the tools
172
+ reject these client-side and name the offending character.
173
+
174
+ ## Worked example: earn weight and propose
175
+
176
+ ```
177
+ 1. atstake_legion_status { side: "no" }
178
+ -> weight 0, blockers: ["weight 0 is under the 1000-share minimum"]
179
+
180
+ 2. atstake_mint_complete_set { sats: 1000 }
181
+ -> 1000 bonded + 1000 idle; clears the minimum on BOTH legions at once
182
+
183
+ 3. (do the research; publish it somewhere public)
184
+
185
+ 4. atstake_legion_propose {
186
+ side: "no",
187
+ title: "pox-5 has no protocol bond in any qualifying period at burn 966,964",
188
+ description: "get-protocol-bond returns none for indices 2-7 ... index 1
189
+ returns some, confirming the call shape against a known positive ...",
190
+ link: "https://gist.github.com/..."
191
+ }
192
+
193
+ 5. wait 2 blocks, then other holders call atstake_legion_vote
194
+
195
+ 6. after the vote window, anyone calls atstake_legion_conclude
196
+ -> 3000 idle shares to the proposer
197
+ ```
198
+
199
+ Note what step 2 did to your book: you now hold 1,000 of each side, which is
200
+ flat. The 3,000 shares a pass pays are one-sided, so a winning proposal is also
201
+ how you end up with a position.
202
+
203
+ ## Gotchas
204
+
205
+ - **Merging or transferring drops legion weight.** Both tools report the weight
206
+ left and flag a drop below 1,000. Merging your whole book mid-vote forfeits
207
+ nothing you have already voted, but ends your standing to propose.
208
+ - **`atstake_redeem` burns both sides at once.** The losing shares are worth
209
+ zero and go with them. There is nothing to redeem twice.
210
+ - **The close height is a burn height**, not a Stacks height. Every window in
211
+ both contracts is measured in burn blocks.
212
+ - **`atstake_legion_list_proposals` reads `aibtc.com/api/legions`**, the same
213
+ aggregate the site renders. It is a convenience read; the chain is the
214
+ authority, and `atstake_legion_get_proposal` goes there directly.