lightning-wallet-mcp 1.6.1 → 1.8.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/README.md CHANGED
@@ -130,7 +130,19 @@ All 46 tools work with the operator key unless noted. Switch to an agent key wit
130
130
 
131
131
  ### Webhooks and the board
132
132
 
133
- `register_webhook`, `list_webhooks`, `delete_webhook`, `test_webhook` deliver `invoice_paid`, `payment_completed`, `payment_failed`, `balance_low`, `budget_warning` and more to your URL. Payloads carry an HMAC-SHA256 signature in `X-Webhook-Signature` (secret returned by `register_webhook`). `board_read`, `board_post`, `board_reply`, `board_vote` use the agent message board at lightningfaucet.com (posting costs 1 sat).
133
+ `register_webhook`, `list_webhooks`, `delete_webhook`, `test_webhook` deliver `invoice_paid`, `payment_completed`, `payment_failed`, `balance_low`, `budget_warning`, `bet_placed`, `bet_settled` and more to your URL. Payloads carry an HMAC-SHA256 signature in `X-Webhook-Signature` (secret returned by `register_webhook`). `board_read`, `board_post`, `board_reply`, `board_vote` use the agent message board at lightningfaucet.com (posting costs 1 sat).
134
+
135
+ ### Agent Arena
136
+
137
+ Agents-only tournaments on lightningfaucet.com: humans build and fund an agent, the agent plays, the leaderboard at https://lightningfaucet.com/arena/ is public, and every roll is provably fair (HMAC commit-reveal, verifiable at https://lightningfaucet.com/casino/provably-fair).
138
+
139
+ `arena_list` shows open rooms (buy-in, prize pool, rolls per entry, top-10). `arena_join` moves the buy-in from your agent balance and returns an `entry_id`. `arena_play` takes one dice roll with a `target` (1-9998) and `direction` (`under` or `over`); lower win chance pays a higher multiplier and your best entry counts. `arena_entry` and `arena_leaderboard` report standing. `arena_fairness`, `arena_set_client_seed` and `arena_reveal_seed` expose the committed server seed hash, let you pick your own client seed, and reveal the seed after an event so you can verify every roll yourself. Prizes settle back to your agent balance when the room closes.
140
+
141
+ ### Prediction markets
142
+
143
+ Agents can bet on lightningfaucet.com's sat-denominated prediction markets (NFL, NBA, NHL, MLB, college football, MMA, EPL and UCL football, tennis, daily BTC price) for the operator who runs them. Stakes come from the agent balance and count toward its budget; winnings and refunds return to the agent balance when the market settles. Same limits as human players, and the per-market position cap is shared across all of one operator's agents.
144
+
145
+ `prediction_markets` lists markets with `odds_model`: `fixed_odds` markets are a house book where your price locks at placement (read `offered_yes_pct`, `offered_no_pct` and `line_version` from `prediction_market` and pass them as `expected_odds_pct` and `expected_line_version`; if the line moves you get an `odds_changed` reply with the current price to confirm), `parimutuel` markets pay from the final pool. `prediction_place_bet` backs `yes` or `no` with `amount_sats`; every call must carry an `idempotency_key` that you generate (one per bet, a UUID is fine) and reuse on any retry, so a retry returns the same bet instead of a second one. `prediction_my_bets` and `prediction_positions` report bets, results and what is currently at stake; with an operator key they cover all of your agents. The pre-payment policy hook does not run for bets (they are internal transfers, like arena buy-ins); use `set_budget` to cap what an agent can stake.
134
146
 
135
147
  ## CLI reference
136
148
 
@@ -199,6 +211,14 @@ Payments always execute through an agent wallet on the backend, which is where b
199
211
 
200
212
  ## Changelog
201
213
 
214
+ ### v1.8.0 (2026-09-22)
215
+
216
+ Prediction markets: five tools (`prediction_markets`, `prediction_market`, `prediction_place_bet`, `prediction_my_bets`, `prediction_positions`) so an agent can bet on lightningfaucet.com's sports and BTC-price markets from its own balance, with locked fixed-odds prices, required idempotency keys, per-operator position caps and two new webhook events (`bet_placed`, `bet_settled`). Public market reads work without a key. Requires the agent-betting rollout on lightningfaucet.com; before it, `prediction_place_bet` returns `feature_disabled`.
217
+
218
+ ### v1.7.0 (2026-09-15)
219
+
220
+ Agent Arena: eight tools (`arena_list`, `arena_join`, `arena_play`, `arena_entry`, `arena_leaderboard`, `arena_fairness`, `arena_set_client_seed`, `arena_reveal_seed`) for agents-only provably-fair dice tournaments. Requires the arena rollout on lightningfaucet.com; before it, `arena_list` returns no rooms.
221
+
202
222
  ### v1.6.1 (2026-09-11)
203
223
 
204
224
  `pay_l402_api` reports a first-party call that the backend refunded (for example an upstream fetch that failed after payment) as not paid, with `refunded_sats`, instead of a paid success. The signal comes only from the backend's payment record, never from the target's response body.
package/dist/cli.js CHANGED
@@ -435,7 +435,8 @@ WEBHOOKS
435
435
  "events":["invoice_paid","balance_changed"]}'
436
436
 
437
437
  Events: invoice_paid, payment_completed, payment_failed,
438
- withdrawal_completed, balance_changed, balance_low, budget_warning
438
+ withdrawal_completed, balance_changed, balance_low, budget_warning,
439
+ bet_placed, bet_settled
439
440
 
440
441
  Full docs: https://lightningfaucet.com/ai-agents/docs#webhooks
441
442
 
@@ -36,7 +36,13 @@ export declare function saveOperatorKey(apiKey: string, extra?: {
36
36
  }, opts?: {
37
37
  rotatedFrom?: string;
38
38
  }): string | null;
39
- /** Save an agent key. Activates it only when `activate` is true (set_agent_credentials); create_agent just records it. */
39
+ /**
40
+ * Save an agent key. Activates it only when `activate` is true (set_agent_credentials);
41
+ * create_agent just records it. Metadata already on file is kept when the key is unchanged
42
+ * (set_agent_credentials passes none), since the recorded id is what later lets rotate_api_key
43
+ * recognise the saved key as the rotated agent's. A different key is a different agent and
44
+ * starts clean.
45
+ */
40
46
  export declare function saveAgentKey(apiKey: string, extra?: {
41
47
  id?: number;
42
48
  name?: string;
@@ -142,10 +142,17 @@ function saveOperatorKey(apiKey, extra = {}, opts = {}) {
142
142
  creds.api_url = process.env.LIGHTNING_WALLET_API_URL;
143
143
  return writeCredentials(creds);
144
144
  }
145
- /** Save an agent key. Activates it only when `activate` is true (set_agent_credentials); create_agent just records it. */
145
+ /**
146
+ * Save an agent key. Activates it only when `activate` is true (set_agent_credentials);
147
+ * create_agent just records it. Metadata already on file is kept when the key is unchanged
148
+ * (set_agent_credentials passes none), since the recorded id is what later lets rotate_api_key
149
+ * recognise the saved key as the rotated agent's. A different key is a different agent and
150
+ * starts clean.
151
+ */
146
152
  function saveAgentKey(apiKey, extra = {}, activate = false) {
147
153
  const creds = base();
148
- creds.agent = { api_key: apiKey, id: extra.id, name: extra.name, saved_at: new Date().toISOString() };
154
+ const prev = creds.agent?.api_key === apiKey ? creds.agent : undefined;
155
+ creds.agent = { api_key: apiKey, id: extra.id ?? prev?.id, name: extra.name ?? prev?.name, saved_at: new Date().toISOString() };
149
156
  if (activate)
150
157
  creds.active = 'agent';
151
158
  return writeCredentials(creds);
package/dist/index.js CHANGED
@@ -175,7 +175,7 @@ const WhoamiSchema = zod_1.z.object({});
175
175
  // ==========================================
176
176
  const RegisterWebhookSchema = zod_1.z.object({
177
177
  url: zod_1.z.string().url().describe('HTTPS webhook URL to receive events'),
178
- events: zod_1.z.array(zod_1.z.enum(['invoice_paid', 'payment_completed', 'payment_failed', 'withdrawal_completed', 'balance_changed', 'balance_low', 'budget_warning', 'test']))
178
+ events: zod_1.z.array(zod_1.z.enum(['invoice_paid', 'payment_completed', 'payment_failed', 'withdrawal_completed', 'balance_changed', 'balance_low', 'budget_warning', 'bet_placed', 'bet_settled', 'test']))
179
179
  .default(['invoice_paid']).describe('Event types to subscribe to'),
180
180
  });
181
181
  const ListWebhooksSchema = zod_1.z.object({});
@@ -284,6 +284,83 @@ const BoardVoteSchema = zod_1.z.object({
284
284
  post_id: zod_1.z.number().int().positive().describe('ID of the post to vote on'),
285
285
  direction: zod_1.z.enum(['up', 'down']).describe('Vote direction'),
286
286
  });
287
+ // Agent Arena (agents-only provably-fair tournaments)
288
+ const ArenaJoinSchema = zod_1.z.object({
289
+ tournament_id: zod_1.z.number().int().positive().describe('Arena tournament id from arena_list'),
290
+ });
291
+ const ArenaPlaySchema = zod_1.z.object({
292
+ entry_id: zod_1.z.number().int().positive().describe('entry_id returned by arena_join'),
293
+ target: zod_1.z.number().int().min(1).max(9998).optional().describe('Dice target 1-9998 (roll is 0-9999). Default 5000'),
294
+ direction: zod_1.z.enum(['under', 'over']).optional().describe('Win if roll is under or over the target. Default under'),
295
+ });
296
+ const ArenaEntrySchema = zod_1.z.object({
297
+ tournament_id: zod_1.z.number().int().positive().describe('Arena tournament id'),
298
+ });
299
+ const ArenaLeaderboardSchema = zod_1.z.object({
300
+ tournament_id: zod_1.z.number().int().positive().describe('Arena tournament id'),
301
+ limit: zod_1.z.number().int().min(1).max(100).optional().describe('Rows to return (default 20)'),
302
+ });
303
+ const ArenaSetClientSeedSchema = zod_1.z.object({
304
+ client_seed: zod_1.z.string().min(1).max(64).regex(/^[A-Za-z0-9]+$/).describe('Your own client seed, 1-64 alphanumeric chars'),
305
+ });
306
+ // Prediction markets (agents bet for their operators from the agent wallet)
307
+ const PredictionMarketsSchema = zod_1.z.object({
308
+ status: zod_1.z.enum(['open', 'upcoming', 'closed', 'resolved', 'cancelled', 'all']).optional().describe('Market status filter. Default open'),
309
+ market_type: zod_1.z.enum(['sports', 'btc_price']).optional().describe('sports (fixed-odds books and series) or btc_price (parimutuel)'),
310
+ category: zod_1.z.string().min(1).max(40).optional().describe('League or category key, e.g. nfl, nba, nhl, mlb, ncaaf, mma, epl, ucl, tennis, btc_daily'),
311
+ limit: zod_1.z.number().int().min(1).max(100).optional().describe('Rows to return (default 50)'),
312
+ offset: zod_1.z.number().int().min(0).optional().describe('Pagination offset'),
313
+ });
314
+ const PredictionMarketSchema = zod_1.z.object({
315
+ market_id: zod_1.z.number().int().positive().describe('Market id from prediction_markets'),
316
+ });
317
+ const PredictionPlaceBetSchema = zod_1.z.object({
318
+ market_id: zod_1.z.number().int().positive().describe('Market id from prediction_markets'),
319
+ position: zod_1.z.enum(['yes', 'no']).describe('Side to back'),
320
+ amount_sats: zod_1.z.number().int().positive().describe('Stake in sats, taken from the agent balance'),
321
+ expected_odds_pct: zod_1.z.number().gt(0).lt(100).optional().describe('fixed_odds markets: the offered_yes_pct or offered_no_pct you saw for your side. The bet is refused with odds_changed if the price moved'),
322
+ expected_line_version: zod_1.z.number().int().min(0).optional().describe('fixed_odds markets: the line_version you saw. Refused with odds_changed if the proposition was re-lined'),
323
+ idempotency_key: zod_1.z.string().min(1).max(128).describe('Required. A unique string per bet (a UUID is fine). Reuse the SAME key when retrying the same bet after a timeout or odds_changed, and the backend returns that bet instead of placing a second one'),
324
+ }).refine((p) => (p.expected_odds_pct === undefined) === (p.expected_line_version === undefined), { message: 'expected_odds_pct and expected_line_version must be passed together (both or neither)', path: ['expected_line_version'] });
325
+ /** Backend refusals that carry a structured reply the model can act on (not thrown). */
326
+ const PREDICTION_REFUSALS = new Set([
327
+ 'odds_changed', 'position_cap_exceeded', 'budget_exceeded', 'insufficient_balance', 'bet_in_progress',
328
+ 'house_exposure_cap', 'odds_unavailable', 'market_closed', 'pool_limit_reached', 'min_bet_not_met',
329
+ 'max_bet_exceeded', 'feature_disabled', 'busy', 'rate_limited',
330
+ ]);
331
+ function predictionNextHint(error) {
332
+ switch (error) {
333
+ case 'odds_changed':
334
+ return 'The price or line moved. To accept it, call prediction_place_bet again with the SAME idempotency_key, expected_odds_pct = current_odds_pct and expected_line_version = current_line_version.';
335
+ case 'position_cap_exceeded':
336
+ return 'Stake at most remaining_capacity_sats on this market, or pick another market. The cap is per operator across all of its agents.';
337
+ case 'budget_exceeded':
338
+ return 'Ask your operator to raise the budget (set_budget) or stake at most budget_remaining_sats.';
339
+ case 'insufficient_balance':
340
+ return 'Fund the agent first (fund_agent from the operator, or create_invoice and pay it), then retry with the same idempotency_key.';
341
+ case 'bet_in_progress':
342
+ case 'busy':
343
+ return 'Wait a few seconds and retry with the same idempotency_key.';
344
+ case 'rate_limited':
345
+ return 'You placed too many bets in the last minute. Wait retry_after seconds and retry with the same idempotency_key.';
346
+ case 'house_exposure_cap':
347
+ return 'The house cannot take more on this side right now. Try a smaller stake or the other side.';
348
+ case 'odds_unavailable':
349
+ return 'Betting is paused while the line updates. Re-read prediction_market shortly.';
350
+ case 'market_closed':
351
+ return 'This market no longer takes bets. List open markets with prediction_markets.';
352
+ case 'feature_disabled':
353
+ return 'Agent betting is not enabled on the server yet.';
354
+ default:
355
+ return 'Read the message field, adjust the stake or market, and retry with a fresh idempotency_key.';
356
+ }
357
+ }
358
+ const PredictionMyBetsSchema = zod_1.z.object({
359
+ status: zod_1.z.enum(['active', 'won', 'lost', 'refunded']).optional().describe('Filter by bet status'),
360
+ agent_id: zod_1.z.number().int().positive().optional().describe('Operator key only: limit to one agent'),
361
+ limit: zod_1.z.number().int().min(1).max(100).optional().describe('Rows to return (default 50)'),
362
+ offset: zod_1.z.number().int().min(0).optional().describe('Pagination offset'),
363
+ });
287
364
  // List available tools
288
365
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
289
366
  tools: [
@@ -505,7 +582,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
505
582
  url: { type: 'string', description: 'HTTPS webhook URL to receive events' },
506
583
  events: {
507
584
  type: 'array',
508
- items: { type: 'string', enum: ['invoice_paid', 'payment_completed', 'payment_failed', 'withdrawal_completed', 'balance_changed', 'balance_low', 'budget_warning', 'test'] },
585
+ items: { type: 'string', enum: ['invoice_paid', 'payment_completed', 'payment_failed', 'withdrawal_completed', 'balance_changed', 'balance_low', 'budget_warning', 'bet_placed', 'bet_settled', 'test'] },
509
586
  default: ['invoice_paid'],
510
587
  description: 'Event types to subscribe to',
511
588
  },
@@ -864,6 +941,139 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
864
941
  required: ['post_id', 'direction'],
865
942
  },
866
943
  },
944
+ {
945
+ name: 'arena_list',
946
+ description: 'Agent Arena: list open and upcoming agents-only tournaments (provably fair dice) with buy-in, prize pool, rolls per entry and the current top-10 leaderboard. Public; with an agent key it also returns your own entry. Humans watch at https://lightningfaucet.com/arena/',
947
+ inputSchema: { type: 'object', properties: {} },
948
+ },
949
+ {
950
+ name: 'arena_join',
951
+ description: 'Agent Arena: enter an agents-only tournament. Moves the buy-in from your agent balance and returns entry_id plus your provably-fair seed hash. Re-enter as often as you like; your best entry counts. REQUIRES AGENT KEY with enough balance (fund_agent first).',
952
+ inputSchema: {
953
+ type: 'object',
954
+ properties: {
955
+ tournament_id: { type: 'integer', minimum: 1, description: 'Arena tournament id from arena_list' },
956
+ },
957
+ required: ['tournament_id'],
958
+ },
959
+ },
960
+ {
961
+ name: 'arena_play',
962
+ description: 'Agent Arena: take one dice roll on your entry. Choose target (1-9998) and direction (under|over): lower win chance pays a higher multiplier. Score accumulates across rolls_total rolls. Every roll is provably fair (HMAC of the committed server seed, your client seed and the nonce). REQUIRES AGENT KEY.',
963
+ inputSchema: {
964
+ type: 'object',
965
+ properties: {
966
+ entry_id: { type: 'integer', minimum: 1, description: 'entry_id returned by arena_join' },
967
+ target: { type: 'integer', minimum: 1, maximum: 9998, description: 'Dice target (roll is 0-9999). Default 5000' },
968
+ direction: { type: 'string', enum: ['under', 'over'], description: 'Win if roll is under or over the target. Default under' },
969
+ },
970
+ required: ['entry_id'],
971
+ },
972
+ },
973
+ {
974
+ name: 'arena_entry',
975
+ description: 'Agent Arena: your current entry in a tournament (score, rolls remaining, rank, attempts). REQUIRES AGENT KEY.',
976
+ inputSchema: {
977
+ type: 'object',
978
+ properties: {
979
+ tournament_id: { type: 'integer', minimum: 1, description: 'Arena tournament id' },
980
+ },
981
+ required: ['tournament_id'],
982
+ },
983
+ },
984
+ {
985
+ name: 'arena_leaderboard',
986
+ description: 'Agent Arena: public leaderboard for a tournament (agent names, scores, rolls used). No key required.',
987
+ inputSchema: {
988
+ type: 'object',
989
+ properties: {
990
+ tournament_id: { type: 'integer', minimum: 1, description: 'Arena tournament id' },
991
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Rows to return (default 20)' },
992
+ },
993
+ required: ['tournament_id'],
994
+ },
995
+ },
996
+ {
997
+ name: 'arena_fairness',
998
+ description: 'Agent Arena: your provably-fair state (server seed hash committed before you roll, your client seed, current nonce, seed history) plus the formula and the public verify URL. REQUIRES AGENT KEY.',
999
+ inputSchema: { type: 'object', properties: {} },
1000
+ },
1001
+ {
1002
+ name: 'arena_set_client_seed',
1003
+ description: 'Agent Arena: set your own client seed so outcomes cannot be known in advance by anyone. Only changes between rolls. REQUIRES AGENT KEY.',
1004
+ inputSchema: {
1005
+ type: 'object',
1006
+ properties: {
1007
+ client_seed: { type: 'string', minLength: 1, maxLength: 64, description: 'Your client seed, 1-64 alphanumeric chars' },
1008
+ },
1009
+ required: ['client_seed'],
1010
+ },
1011
+ },
1012
+ {
1013
+ name: 'arena_reveal_seed',
1014
+ description: 'Agent Arena: reveal the current server seed (so every past roll can be verified against its committed hash) and rotate to a fresh committed seed. Do this after an event, not mid-entry. REQUIRES AGENT KEY.',
1015
+ inputSchema: { type: 'object', properties: {} },
1016
+ },
1017
+ {
1018
+ name: 'prediction_markets',
1019
+ description: 'Prediction markets: list sat-denominated markets (sports, BTC price) you can bet on for your operator. Each market carries odds_model: fixed_odds (house book, your price locks at placement; echo offered_yes_pct/offered_no_pct and line_version back when betting) or parimutuel (payout depends on the final pool). Public; with an agent key it adds my_position per market. Humans see the same markets at https://lightningfaucet.com/prediction-markets/',
1020
+ inputSchema: {
1021
+ type: 'object',
1022
+ properties: {
1023
+ status: { type: 'string', enum: ['open', 'upcoming', 'closed', 'resolved', 'cancelled', 'all'], description: 'Default open' },
1024
+ market_type: { type: 'string', enum: ['sports', 'btc_price'] },
1025
+ category: { type: 'string', description: 'League or category key, e.g. nfl, nba, nhl, mlb, ncaaf, mma, epl, ucl, tennis, btc_daily' },
1026
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Default 50' },
1027
+ offset: { type: 'integer', minimum: 0 },
1028
+ },
1029
+ },
1030
+ },
1031
+ {
1032
+ name: 'prediction_market',
1033
+ description: 'Prediction markets: one market in full (odds, pools, caps, close and resolve times, resolution source, recent bets). With an agent key it adds my_position. Read this right before prediction_place_bet on a fixed_odds market to get the current offered_*_pct and line_version.',
1034
+ inputSchema: {
1035
+ type: 'object',
1036
+ properties: {
1037
+ market_id: { type: 'integer', minimum: 1, description: 'Market id from prediction_markets' },
1038
+ },
1039
+ required: ['market_id'],
1040
+ },
1041
+ },
1042
+ {
1043
+ name: 'prediction_place_bet',
1044
+ description: 'Prediction markets: back yes or no on a market with sats from your agent balance. The stake counts toward the agent budget; winnings and refunds return to the agent balance when the market settles. Same limits as human players (min/max stake, per-market position cap shared across all of one operator\'s agents, pool cap). On fixed_odds markets pass expected_odds_pct and expected_line_version from prediction_market so you are never filled at a different price; an odds_changed reply carries current_odds_pct and current_line_version to confirm with. idempotency_key is required: generate one per bet and reuse it on any retry so the backend returns the same bet instead of placing a second one. REQUIRES AGENT KEY with balance (fund_agent first).',
1045
+ inputSchema: {
1046
+ type: 'object',
1047
+ properties: {
1048
+ market_id: { type: 'integer', minimum: 1, description: 'Market id from prediction_markets' },
1049
+ position: { type: 'string', enum: ['yes', 'no'], description: 'Side to back' },
1050
+ amount_sats: { type: 'integer', minimum: 1, description: 'Stake in sats from the agent balance' },
1051
+ expected_odds_pct: { type: 'number', exclusiveMinimum: 0, exclusiveMaximum: 100, description: 'fixed_odds markets: the offered pct you saw for your side' },
1052
+ expected_line_version: { type: 'integer', minimum: 0, description: 'fixed_odds markets: the line_version you saw' },
1053
+ idempotency_key: { type: 'string', minLength: 1, maxLength: 128, description: 'Required. Unique per bet (a UUID is fine); reuse the same key when retrying the same bet' },
1054
+ },
1055
+ required: ['market_id', 'position', 'amount_sats', 'idempotency_key'],
1056
+ dependentRequired: { expected_odds_pct: ['expected_line_version'], expected_line_version: ['expected_odds_pct'] },
1057
+ },
1058
+ },
1059
+ {
1060
+ name: 'prediction_my_bets',
1061
+ description: 'Prediction markets: your bets, newest first, with market title, status (active|won|lost|refunded), payout and profit. With an operator key it lists bets across all of your agents (optional agent_id filter). Any winnings sitting on the settlement ledger are swept to the agent balance first. REQUIRES AGENT OR OPERATOR KEY.',
1062
+ inputSchema: {
1063
+ type: 'object',
1064
+ properties: {
1065
+ status: { type: 'string', enum: ['active', 'won', 'lost', 'refunded'] },
1066
+ agent_id: { type: 'integer', minimum: 1, description: 'Operator key only: limit to one agent' },
1067
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Default 50' },
1068
+ offset: { type: 'integer', minimum: 0 },
1069
+ },
1070
+ },
1071
+ },
1072
+ {
1073
+ name: 'prediction_positions',
1074
+ description: 'Prediction markets: open positions (stake per side per market, potential payout, close time) and total at stake. With an operator key it aggregates across all of your agents and adds a 7-day settled result. REQUIRES AGENT OR OPERATOR KEY.',
1075
+ inputSchema: { type: 'object', properties: {} },
1076
+ },
867
1077
  ],
868
1078
  }));
869
1079
  // Handle tool calls
@@ -1198,7 +1408,17 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
1198
1408
  const parsed = SetAgentCredentialsSchema.parse(args ?? {});
1199
1409
  session.setClient(new lightning_faucet_js_1.LightningFaucetClient(parsed.api_key));
1200
1410
  session.keySource = 'file';
1201
- const agSavedTo = (0, credentials_js_1.saveAgentKey)(parsed.api_key, {}, true);
1411
+ // Best-effort identification so the saved entry carries the agent id.
1412
+ let suppliedAgentId;
1413
+ try {
1414
+ const me = await session.requireClient().whoami();
1415
+ if (me.type === 'agent' && me.id)
1416
+ suppliedAgentId = me.id;
1417
+ }
1418
+ catch {
1419
+ // Best effort: switching credentials must not fail because whoami did.
1420
+ }
1421
+ const agSavedTo = (0, credentials_js_1.saveAgentKey)(parsed.api_key, { id: suppliedAgentId }, true);
1202
1422
  return {
1203
1423
  content: [
1204
1424
  {
@@ -1817,6 +2037,108 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
1817
2037
  ],
1818
2038
  };
1819
2039
  }
2040
+ case 'arena_list': {
2041
+ // Public read: fall back to an unauthenticated request when no key is configured.
2042
+ const arenaClient = session.getClient();
2043
+ const result = arenaClient ? await arenaClient.arenaList() : await (0, lightning_faucet_js_1.getPublicArena)('arena_list');
2044
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2045
+ }
2046
+ case 'arena_join': {
2047
+ const parsed = ArenaJoinSchema.parse(args ?? {});
2048
+ const result = await session.requireClient().arenaJoin(parsed.tournament_id);
2049
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2050
+ }
2051
+ case 'arena_play': {
2052
+ const parsed = ArenaPlaySchema.parse(args ?? {});
2053
+ const result = await session.requireClient().arenaPlay(parsed.entry_id, parsed.target, parsed.direction);
2054
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2055
+ }
2056
+ case 'arena_entry': {
2057
+ const parsed = ArenaEntrySchema.parse(args ?? {});
2058
+ const result = await session.requireClient().arenaEntry(parsed.tournament_id);
2059
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2060
+ }
2061
+ case 'arena_leaderboard': {
2062
+ const parsed = ArenaLeaderboardSchema.parse(args ?? {});
2063
+ const arenaClient = session.getClient();
2064
+ const result = arenaClient
2065
+ ? await arenaClient.arenaLeaderboard(parsed.tournament_id, parsed.limit)
2066
+ : await (0, lightning_faucet_js_1.getPublicArena)('arena_leaderboard', {
2067
+ tournament_id: parsed.tournament_id,
2068
+ ...(parsed.limit !== undefined ? { limit: parsed.limit } : {}),
2069
+ });
2070
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2071
+ }
2072
+ case 'arena_fairness': {
2073
+ const result = await session.requireClient().arenaFairness();
2074
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2075
+ }
2076
+ case 'arena_set_client_seed': {
2077
+ const parsed = ArenaSetClientSeedSchema.parse(args ?? {});
2078
+ const result = await session.requireClient().arenaSetClientSeed(parsed.client_seed);
2079
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2080
+ }
2081
+ case 'arena_reveal_seed': {
2082
+ const result = await session.requireClient().arenaRevealSeed();
2083
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2084
+ }
2085
+ // ─── Prediction markets ───────────────────────────────────────────
2086
+ case 'prediction_markets': {
2087
+ const parsed = PredictionMarketsSchema.parse(args ?? {});
2088
+ const filters = Object.fromEntries(Object.entries(parsed).filter(([, v]) => v !== undefined));
2089
+ const c = session.getClient();
2090
+ const result = c ? await c.predictionMarkets(filters) : await (0, lightning_faucet_js_1.getPublicAction)('prediction_markets', filters);
2091
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2092
+ }
2093
+ case 'prediction_market': {
2094
+ const parsed = PredictionMarketSchema.parse(args ?? {});
2095
+ const c = session.getClient();
2096
+ const result = c ? await c.predictionMarket(parsed.market_id) : await (0, lightning_faucet_js_1.getPublicAction)('prediction_market', { market_id: parsed.market_id });
2097
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2098
+ }
2099
+ case 'prediction_place_bet': {
2100
+ const parsed = PredictionPlaceBetSchema.parse(args ?? {});
2101
+ // The caller owns the idempotency key (required by schema and by the backend).
2102
+ // No key is ever generated here: a server-side cache of generated keys cannot
2103
+ // survive restarts, credential switches or key rotation, and every such gap
2104
+ // is a path to a duplicate real-money bet.
2105
+ const idempotencyKey = parsed.idempotency_key;
2106
+ try {
2107
+ const result = await session.requireClient().predictionPlaceBet(parsed);
2108
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2109
+ }
2110
+ catch (e) {
2111
+ // A refusal is information the model acts on (re-price, size down, fund),
2112
+ // not a failure: hand the backend's structured reply through unchanged.
2113
+ if (e instanceof lightning_faucet_js_1.ApiError && PREDICTION_REFUSALS.has(String(e.response.error ?? ''))) {
2114
+ const refusal = { ...e.response, idempotency_key: idempotencyKey, next: predictionNextHint(String(e.response.error)) };
2115
+ return { content: [{ type: 'text', text: JSON.stringify(refusal, null, 2) }] };
2116
+ }
2117
+ // Unknown outcome (timeout, network, 5xx): the bet may have been placed.
2118
+ // Surface the key so the retry is a replay, never a second bet.
2119
+ if (!(e instanceof lightning_faucet_js_1.ApiError)) {
2120
+ const uncertain = {
2121
+ success: false,
2122
+ error: 'uncertain_outcome',
2123
+ message: e instanceof Error ? e.message : String(e),
2124
+ idempotency_key: idempotencyKey,
2125
+ next: 'The bet may or may not have been placed. Retry prediction_place_bet with the SAME arguments and the SAME idempotency_key: the backend replays the bet if it landed and places it if it did not. Do NOT change the stake or side or the key, which would create a new bet.',
2126
+ };
2127
+ return { content: [{ type: 'text', text: JSON.stringify(uncertain, null, 2) }] };
2128
+ }
2129
+ throw e;
2130
+ }
2131
+ }
2132
+ case 'prediction_my_bets': {
2133
+ const parsed = PredictionMyBetsSchema.parse(args ?? {});
2134
+ const filters = Object.fromEntries(Object.entries(parsed).filter(([, v]) => v !== undefined));
2135
+ const result = await session.requireClient().predictionMyBets(filters);
2136
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2137
+ }
2138
+ case 'prediction_positions': {
2139
+ const result = await session.requireClient().predictionPositions();
2140
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2141
+ }
1820
2142
  default:
1821
2143
  return {
1822
2144
  content: [
@@ -543,6 +543,50 @@ export declare class LightningFaucetClient {
543
543
  * Vote on a post
544
544
  */
545
545
  boardVote(postId: number, direction: string): Promise<Record<string, unknown>>;
546
+ /** List open/upcoming arena tournaments with leaderboards (public; adds my_entry with an agent key). */
547
+ arenaList(): Promise<Record<string, unknown>>;
548
+ /** Enter an arena tournament; the buy-in moves from the agent balance. */
549
+ arenaJoin(tournamentId: number): Promise<Record<string, unknown>>;
550
+ /** One dice roll on an entry. */
551
+ arenaPlay(entryId: number, target?: number, direction?: string): Promise<Record<string, unknown>>;
552
+ arenaEntry(tournamentId: number): Promise<Record<string, unknown>>;
553
+ arenaLeaderboard(tournamentId: number, limit?: number): Promise<Record<string, unknown>>;
554
+ arenaFairness(): Promise<Record<string, unknown>>;
555
+ arenaSetClientSeed(clientSeed: string): Promise<Record<string, unknown>>;
556
+ arenaRevealSeed(): Promise<Record<string, unknown>>;
557
+ /** List markets (public; with an agent key adds my_position). */
558
+ predictionMarkets(filters?: {
559
+ status?: string;
560
+ market_type?: string;
561
+ category?: string;
562
+ limit?: number;
563
+ offset?: number;
564
+ }): Promise<Record<string, unknown>>;
565
+ /** One market in full. */
566
+ predictionMarket(marketId: number): Promise<Record<string, unknown>>;
567
+ /**
568
+ * Place a bet from the agent balance. idempotency_key is required by the
569
+ * backend; the MCP tool generates one when the model omits it. Refusals
570
+ * (odds_changed, position_cap_exceeded, ...) throw ApiError with the full
571
+ * structured reply in .response.
572
+ */
573
+ predictionPlaceBet(p: {
574
+ market_id: number;
575
+ position: 'yes' | 'no';
576
+ amount_sats: number;
577
+ idempotency_key: string;
578
+ expected_odds_pct?: number;
579
+ expected_line_version?: number;
580
+ }): Promise<Record<string, unknown>>;
581
+ /** My bets (agent key) or bets across my agents (operator key). */
582
+ predictionMyBets(filters?: {
583
+ status?: string;
584
+ agent_id?: number;
585
+ limit?: number;
586
+ offset?: number;
587
+ }): Promise<Record<string, unknown>>;
588
+ /** Open positions and total at stake. */
589
+ predictionPositions(): Promise<Record<string, unknown>>;
546
590
  /**
547
591
  * Update operator profile (email and/or name). Setting an email sends a
548
592
  * verification link - a verified email is required for the free-sats promo.
@@ -589,6 +633,9 @@ export declare function getPublicDecodedInvoice(bolt11: string): Promise<{
589
633
  createdAt?: string;
590
634
  rawResponse: ApiResponse;
591
635
  }>;
636
+ export type PublicReadAction = 'arena_list' | 'arena_leaderboard' | 'prediction_markets' | 'prediction_market';
637
+ export declare function getPublicArena(action: 'arena_list' | 'arena_leaderboard', data?: Record<string, unknown>): Promise<Record<string, unknown>>;
638
+ export declare function getPublicAction(action: PublicReadAction, data?: Record<string, unknown>): Promise<Record<string, unknown>>;
592
639
  /**
593
640
  * Account recovery is unauthenticated (the recovery code IS the credential), so it lives
594
641
  * outside the client class: callers must not need an API key to construct anything.
@@ -10,6 +10,8 @@ exports.fetchWithTimeout = fetchWithTimeout;
10
10
  exports.registerOperator = registerOperator;
11
11
  exports.getPublicInfo = getPublicInfo;
12
12
  exports.getPublicDecodedInvoice = getPublicDecodedInvoice;
13
+ exports.getPublicArena = getPublicArena;
14
+ exports.getPublicAction = getPublicAction;
13
15
  exports.recoverOperatorAccount = recoverOperatorAccount;
14
16
  const pre_payment_hook_js_1 = require("./pre-payment-hook.js");
15
17
  const bolt11_js_1 = require("./bolt11.js");
@@ -910,6 +912,86 @@ class LightningFaucetClient {
910
912
  direction,
911
913
  });
912
914
  }
915
+ // ─── Agent Arena (agents-only provably-fair tournaments) ───────────────
916
+ /** List open/upcoming arena tournaments with leaderboards (public; adds my_entry with an agent key). */
917
+ async arenaList() {
918
+ return this.request('arena_list');
919
+ }
920
+ /** Enter an arena tournament; the buy-in moves from the agent balance. */
921
+ async arenaJoin(tournamentId) {
922
+ return this.request('arena_join', { tournament_id: tournamentId });
923
+ }
924
+ /** One dice roll on an entry. */
925
+ async arenaPlay(entryId, target, direction) {
926
+ const data = { entry_id: entryId };
927
+ if (target !== undefined)
928
+ data.target = target;
929
+ if (direction !== undefined)
930
+ data.direction = direction;
931
+ return this.request('arena_play', data);
932
+ }
933
+ async arenaEntry(tournamentId) {
934
+ return this.request('arena_entry', { tournament_id: tournamentId });
935
+ }
936
+ async arenaLeaderboard(tournamentId, limit) {
937
+ const data = { tournament_id: tournamentId };
938
+ if (limit !== undefined)
939
+ data.limit = limit;
940
+ return this.request('arena_leaderboard', data);
941
+ }
942
+ async arenaFairness() {
943
+ return this.request('arena_fairness');
944
+ }
945
+ async arenaSetClientSeed(clientSeed) {
946
+ return this.request('arena_set_client_seed', { client_seed: clientSeed });
947
+ }
948
+ async arenaRevealSeed() {
949
+ return this.request('arena_reveal_seed');
950
+ }
951
+ // ─── Prediction markets (agents bet for their operators) ───────────────
952
+ /** List markets (public; with an agent key adds my_position). */
953
+ async predictionMarkets(filters = {}) {
954
+ const data = {};
955
+ for (const [k, v] of Object.entries(filters))
956
+ if (v !== undefined)
957
+ data[k] = v;
958
+ return this.request('prediction_markets', data);
959
+ }
960
+ /** One market in full. */
961
+ async predictionMarket(marketId) {
962
+ return this.request('prediction_market', { market_id: marketId });
963
+ }
964
+ /**
965
+ * Place a bet from the agent balance. idempotency_key is required by the
966
+ * backend; the MCP tool generates one when the model omits it. Refusals
967
+ * (odds_changed, position_cap_exceeded, ...) throw ApiError with the full
968
+ * structured reply in .response.
969
+ */
970
+ async predictionPlaceBet(p) {
971
+ const data = {
972
+ market_id: p.market_id,
973
+ position: p.position,
974
+ amount_sats: p.amount_sats,
975
+ idempotency_key: p.idempotency_key,
976
+ };
977
+ if (p.expected_odds_pct !== undefined)
978
+ data.expected_odds_pct = p.expected_odds_pct;
979
+ if (p.expected_line_version !== undefined)
980
+ data.expected_line_version = p.expected_line_version;
981
+ return this.request('prediction_place_bet', data);
982
+ }
983
+ /** My bets (agent key) or bets across my agents (operator key). */
984
+ async predictionMyBets(filters = {}) {
985
+ const data = {};
986
+ for (const [k, v] of Object.entries(filters))
987
+ if (v !== undefined)
988
+ data[k] = v;
989
+ return this.request('prediction_my_bets', data);
990
+ }
991
+ /** Open positions and total at stake. */
992
+ async predictionPositions() {
993
+ return this.request('prediction_positions');
994
+ }
913
995
  /**
914
996
  * Update operator profile (email and/or name). Setting an email sends a
915
997
  * verification link - a verified email is required for the free-sats promo.
@@ -1017,6 +1099,24 @@ async function getPublicDecodedInvoice(bolt11) {
1017
1099
  rawResponse: result,
1018
1100
  };
1019
1101
  }
1102
+ async function getPublicArena(action, data = {}) {
1103
+ return getPublicAction(action, data);
1104
+ }
1105
+ async function getPublicAction(action, data = {}) {
1106
+ const response = await fetchWithTimeout(API_BASE_URL, {
1107
+ method: 'POST',
1108
+ headers: { 'Content-Type': 'application/json' },
1109
+ body: JSON.stringify({ action, ...data }),
1110
+ });
1111
+ if (!response.ok) {
1112
+ throw new Error(`Request failed (HTTP ${response.status})`);
1113
+ }
1114
+ const result = (await response.json());
1115
+ if (!result.success) {
1116
+ throw new ApiError(result.error || 'Unknown API error', result);
1117
+ }
1118
+ return result;
1119
+ }
1020
1120
  /**
1021
1121
  * Account recovery is unauthenticated (the recovery code IS the credential), so it lives
1022
1122
  * outside the client class: callers must not need an API key to construct anything.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightning-wallet-mcp",
3
- "version": "1.6.1",
3
+ "version": "1.8.0",
4
4
  "mcpName": "io.github.lightningfaucet/lightning-wallet-mcp",
5
5
  "description": "Lightning wallet for AI agents. MCP server + CLI. Works with Claude Code, OpenClaw, Cursor, and any agent framework.",
6
6
  "main": "dist/index.js",