lightning-wallet-mcp 1.9.0 → 1.11.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
@@ -47,6 +47,19 @@ That is it. `register_operator` saves your credentials to `~/.lightning-wallet/c
47
47
  }
48
48
  ```
49
49
 
50
+ **Remote server (no install)**: hosted AI apps and agent frameworks that speak MCP over HTTP can connect to
51
+
52
+ ```
53
+ https://lightningfaucet.com/mcp
54
+ ```
55
+
56
+ It is a stateless Streamable HTTP endpoint with the same tools. Without a key you get the public read-only set (`get_info`, `decode_invoice`, `list_l402_apis`, `verify_fair`, `prediction_markets`, `prediction_market`, `arena_list`, `arena_leaderboard`, `gateway_catalog`, `pickem_browse`, `tournament_list`). For everything else send your own key on every request as `Authorization: Bearer <key>`; an agent key (`agent_...`) with a budget and spend policy is the safe choice. The server stores no keys, so `register_operator`, `recover_account` and the local credential tools are stdio only: create a key at [lightningfaucet.com/ai-agents](https://lightningfaucet.com/ai-agents/) or with the local package first.
57
+
58
+ ```bash
59
+ claude mcp add --transport http lightning-wallet https://lightningfaucet.com/mcp \
60
+ --header "Authorization: Bearer agent_your_agent_key"
61
+ ```
62
+
50
63
  **CLI** (any agent framework, CI, or a plain shell):
51
64
 
52
65
  ```bash
@@ -144,6 +157,26 @@ Agents can bet on lightningfaucet.com's sat-denominated prediction markets (NFL,
144
157
 
145
158
  `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.
146
159
 
160
+ ### Reporting, discovery and selling (v1.10)
161
+
162
+ | Tool | Key | What it does |
163
+ |------|-----|--------------|
164
+ | `export_transactions` | agent | Transaction history as JSON or CSV for a date range (truncated past about 60k characters) |
165
+ | `get_agent_analytics` | agent | Spent, received, count, average payment and top L402 destinations over 24h, 7d, 30d or all |
166
+ | `link_user` | operator | Link the operator to a website account by recovery code. `confirm: true` required; `merge_balance` defaults to false |
167
+ | `get_spend_policy` | agent or operator | Read per-transaction, daily, per-market and responsible-gambling limits on an agent |
168
+ | `set_spend_policy` | operator | Set those limits. Tightening is immediate; loosening a loss cap or cooldown takes effect after 24 hours |
169
+ | `list_l402_apis` | none | Find L402 APIs to pay for: `first_party` (our priced endpoints), `registry`, `discovered`, `gateway` |
170
+ | `verify_fair` | none | Recompute a provably fair roll locally (HMAC-SHA256), check the seed commitment and a claimed result |
171
+ | `pickem_browse` | none | Read this week's Pick'em slate, results and prizes (entry needs a person's account) |
172
+ | `tournament_list` | none | Read the human casino tournaments (agents enter the Arena instead) |
173
+ | `gateway_catalog` | none | Browse seller endpoints on our L402 gateway |
174
+ | `gateway_probe`, `gateway_register`, `gateway_list`, `gateway_update`, `gateway_pause`, `gateway_resume`, `gateway_delete`, `gateway_stats` | operator | Sell your own API behind an L402 paywall. `gateway_delete` needs `confirm: true` |
175
+
176
+ When a limit blocks a spend the error is typed (`per_tx_cap_exceeded`, `daily_cap_exceeded`, `per_market_cap_exceeded`, `loss_cap_reached`, `cooldown_active`) with the cap and what is left. New agents start with conservative defaults (5,000 sats per transaction, 50,000 per day, 10,000 per market, 10,000 daily loss cap, 60 minute cooldown).
177
+
178
+ There is intentionally no faucet or free-spin claim tool: those are one per person and gated by email, device and IP checks, so an agent claiming would be farming. Pick'em entry and human tournament join also need a person's website account.
179
+
147
180
  ## CLI reference
148
181
 
149
182
  ```
@@ -196,6 +229,11 @@ Set `PRE_PAYMENT_HOOK_URL` and every outgoing payment (`pay_l402_api`, `pay_invo
196
229
  - Keep the recovery code offline. It is the only way back in if the key is lost.
197
230
  - Use agent keys with budgets for anything autonomous; the operator key can withdraw.
198
231
  - Verify webhook payloads: compare `X-Webhook-Signature` with the HMAC-SHA256 of the raw body under your webhook secret.
232
+ - Remote server: keys travel only in the `Authorization` header over HTTPS and are never stored or logged (logs carry a short hash). It refuses key-management tools, rate limits per IP and per key, caps request and response size, validates `Origin`, and only ever talks to the Lightning Faucet API. Operators of the server can switch off every money-moving tool at once.
233
+
234
+ ### Self-hosting the remote server
235
+
236
+ `lightning-wallet-mcp-http` (`dist/http-server.js`) runs the Streamable HTTP server. It listens on `LF_MCP_HOST:LF_MCP_PORT` (default `127.0.0.1:8893`) and is meant to sit behind a reverse proxy that sets `X-Real-IP`. Main settings: `LIGHTNING_WALLET_API_URL`, `LF_MCP_TRUSTED_PROXIES`, `LF_MCP_ALLOWED_HOSTS`, `LF_MCP_ALLOWED_ORIGINS`, `LF_MCP_MAX_BODY_BYTES`, the `LF_MCP_RL_*` rate limits, and the kill switch (`LF_MCP_KILL_SWITCH=1`, or create the file named by `LF_MCP_KILL_SWITCH_FILE`). `GET /health` reports status. `PRE_PAYMENT_HOOK_URL` must not be set on a shared server.
199
237
 
200
238
  ## Architecture
201
239
 
@@ -211,6 +249,14 @@ Payments always execute through an agent wallet on the backend, which is where b
211
249
 
212
250
  ## Changelog
213
251
 
252
+ ### v1.11.0 (2026-10-24)
253
+
254
+ Remote MCP server: the same tools over stateless Streamable HTTP at `https://lightningfaucet.com/mcp`, authenticated with your own key in `Authorization: Bearer`, with a public read-only set for anonymous callers. Tool definitions and handlers moved to a shared module (`src/tools.ts`) used by both the stdio and HTTP entrypoints; stdio behaviour is unchanged. `server.json` now lists the remote.
255
+
256
+ ### v1.10.0 (2026-10-24)
257
+
258
+ Eighteen tools: `export_transactions`, `get_agent_analytics`, `link_user`, `list_l402_apis`, `verify_fair`, `pickem_browse`, `tournament_list`, `gateway_catalog`, `get_spend_policy`, `set_spend_policy` and the seller set `gateway_probe`, `gateway_register`, `gateway_list`, `gateway_update`, `gateway_pause`, `gateway_resume`, `gateway_delete`, `gateway_stats`. Destructive calls need `confirm: true`; agent-only tools explain themselves when called with an operator key.
259
+
214
260
  ### v1.8.0 (2026-09-22)
215
261
 
216
262
  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`.
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { LightningFaucetClient } from './lightning-faucet.js';
3
+ type ToolDef = {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: Record<string, unknown>;
7
+ };
8
+ type Session = {
9
+ getClient(): LightningFaucetClient | null;
10
+ requireClient(): LightningFaucetClient;
11
+ };
12
+ type ToolResult = {
13
+ content: {
14
+ type: 'text';
15
+ text: string;
16
+ }[];
17
+ isError?: boolean;
18
+ };
19
+ declare const VerifyFairSchema: z.ZodObject<{
20
+ server_seed: z.ZodString;
21
+ client_seed: z.ZodString;
22
+ nonce: z.ZodNumber;
23
+ server_seed_hash: z.ZodOptional<z.ZodString>;
24
+ claimed_result: z.ZodOptional<z.ZodNumber>;
25
+ result_type: z.ZodOptional<z.ZodEnum<["dice", "roll"]>>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ client_seed: string;
28
+ server_seed: string;
29
+ nonce: number;
30
+ server_seed_hash?: string | undefined;
31
+ claimed_result?: number | undefined;
32
+ result_type?: "dice" | "roll" | undefined;
33
+ }, {
34
+ client_seed: string;
35
+ server_seed: string;
36
+ nonce: number;
37
+ server_seed_hash?: string | undefined;
38
+ claimed_result?: number | undefined;
39
+ result_type?: "dice" | "roll" | undefined;
40
+ }>;
41
+ export declare const AGENT_TOOLS: ToolDef[];
42
+ export declare const AGENT_TOOL_NAMES: Set<string>;
43
+ /** Pure, exported for tests. */
44
+ export declare function verifyFair(p: z.infer<typeof VerifyFairSchema>): Record<string, unknown>;
45
+ /** Returns a result for tools owned by this module, or null so the caller continues. */
46
+ export declare function handleAgentTool(name: string, args: unknown, session: Session): Promise<ToolResult | null>;
47
+ export {};
@@ -0,0 +1,493 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENT_TOOL_NAMES = exports.AGENT_TOOLS = void 0;
4
+ exports.verifyFair = verifyFair;
5
+ exports.handleAgentTool = handleAgentTool;
6
+ /**
7
+ * Agent-coverage tools (v1.10): actions the Lightning Faucet API already serves that
8
+ * had no MCP tool, plus L402 discovery, generic provably-fair verification, and read-only
9
+ * views of Pick'em and the human tournament lobby.
10
+ *
11
+ * Kept in its own module so the big tool switch in index.ts only gains a dispatch hook.
12
+ *
13
+ * Deliberately NOT included:
14
+ * - Free-spin / faucet claims: one claim per human account, gated by email, device and IP
15
+ * checks. An agent claiming would be farming by definition, so there is no agent path.
16
+ * - Pick'em entry and human tournament join: both need a person's account session and pay
17
+ * bonus sats, which would open multi-account farming. Agents have the Agent Arena for
18
+ * tournaments and prediction markets for forecasting; these tools are read-only views.
19
+ */
20
+ const node_crypto_1 = require("node:crypto");
21
+ const zod_1 = require("zod");
22
+ const lightning_faucet_js_1 = require("./lightning-faucet.js");
23
+ const PLATFORM_API_URL = process.env.LIGHTNING_WALLET_PLATFORM_URL || 'https://lightningfaucet.com/api/';
24
+ const L402_WELL_KNOWN_URL = 'https://lightningfaucet.com/.well-known/l402.json';
25
+ /** Cap on text returned in one tool result so a huge export cannot flood the model context. */
26
+ const MAX_TEXT_CHARS = 60_000;
27
+ const ok = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] });
28
+ const typedError = (error, message, extra = {}) => ({
29
+ content: [{ type: 'text', text: JSON.stringify({ success: false, error, message, ...extra }, null, 2) }],
30
+ isError: true,
31
+ });
32
+ // ─── Schemas ────────────────────────────────────────────────────────────
33
+ const LinkUserSchema = zod_1.z.object({
34
+ recovery_code: zod_1.z.string().min(8).max(128).describe('Recovery code of the Lightning Faucet website account to link'),
35
+ merge_balance: zod_1.z.boolean().default(false).describe('Also move this operator balance into the website account. Default false'),
36
+ confirm: zod_1.z.literal(true).describe('Must be true. Linking is permanent and a balance merge cannot be undone'),
37
+ });
38
+ const ExportTransactionsSchema = zod_1.z.object({
39
+ format: zod_1.z.enum(['json', 'csv']).default('json').describe('Output format'),
40
+ start_date: zod_1.z.string().regex(/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/).optional().describe('Inclusive start, YYYY-MM-DD'),
41
+ end_date: zod_1.z.string().regex(/^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/).optional().describe('Inclusive end, YYYY-MM-DD'),
42
+ include_pending: zod_1.z.boolean().default(false).describe('Include pending transactions'),
43
+ });
44
+ const AgentAnalyticsSchema = zod_1.z.object({
45
+ period: zod_1.z.enum(['24h', '7d', '30d', 'all']).default('30d').describe('Reporting window'),
46
+ });
47
+ const GatewayCatalogSchema = zod_1.z.object({
48
+ category: zod_1.z.string().min(1).max(40).optional().describe('Category filter'),
49
+ page: zod_1.z.number().int().min(1).default(1),
50
+ page_size: zod_1.z.number().int().min(1).max(50).default(20),
51
+ });
52
+ const GatewayRegisterSchema = zod_1.z.object({
53
+ name: zod_1.z.string().min(1).max(100).describe('Public name of the paid endpoint'),
54
+ description: zod_1.z.string().max(500).optional(),
55
+ upstream_url: zod_1.z.string().url().describe('Public URL your endpoint proxies to. Private and localhost addresses are refused'),
56
+ upstream_method: zod_1.z.enum(['GET', 'POST', 'PUT', 'DELETE']).default('GET'),
57
+ price_sats: zod_1.z.number().int().positive().describe('Price per call in sats'),
58
+ category: zod_1.z.string().min(1).max(40).optional().describe('Catalog category'),
59
+ upstream_headers: zod_1.z.record(zod_1.z.string()).optional().describe('Headers sent to your upstream (for example its own auth)'),
60
+ upstream_timeout_ms: zod_1.z.number().int().min(1000).max(30000).optional(),
61
+ });
62
+ const GatewayUpdateSchema = zod_1.z.object({
63
+ endpoint_id: zod_1.z.number().int().positive(),
64
+ name: zod_1.z.string().min(1).max(100).optional(),
65
+ description: zod_1.z.string().max(500).optional(),
66
+ price_sats: zod_1.z.number().int().positive().optional(),
67
+ upstream_url: zod_1.z.string().url().optional(),
68
+ upstream_method: zod_1.z.enum(['GET', 'POST', 'PUT', 'DELETE']).optional(),
69
+ upstream_headers: zod_1.z.record(zod_1.z.string()).optional(),
70
+ upstream_timeout_ms: zod_1.z.number().int().min(1000).max(30000).optional(),
71
+ });
72
+ const GatewayIdSchema = zod_1.z.object({ endpoint_id: zod_1.z.number().int().positive().describe('Endpoint id from gateway_list') });
73
+ const GatewayDeleteSchema = zod_1.z.object({
74
+ endpoint_id: zod_1.z.number().int().positive(),
75
+ confirm: zod_1.z.literal(true).describe('Must be true. Deleting an endpoint frees its slug and stops all sales'),
76
+ });
77
+ const GatewayProbeSchema = zod_1.z.object({
78
+ upstream_url: zod_1.z.string().url(),
79
+ upstream_method: zod_1.z.enum(['GET', 'POST', 'PUT', 'DELETE']).default('GET'),
80
+ upstream_headers: zod_1.z.record(zod_1.z.string()).optional(),
81
+ });
82
+ const ListL402Schema = zod_1.z.object({
83
+ source: zod_1.z.enum(['first_party', 'registry', 'discovered', 'gateway']).default('registry')
84
+ .describe('first_party: Lightning Faucet own priced endpoints. registry: community directory. discovered: endpoints seen in real L402 payments. gateway: seller endpoints on our gateway'),
85
+ category: zod_1.z.string().min(1).max(40).optional().describe('registry and gateway: category filter (registry: demo, finance, data, ai, other)'),
86
+ search: zod_1.z.string().min(1).max(100).optional().describe('registry: match on name or provider'),
87
+ limit: zod_1.z.number().int().min(1).max(50).default(20).describe('Rows per page'),
88
+ page: zod_1.z.number().int().min(1).default(1),
89
+ });
90
+ const VerifyFairSchema = zod_1.z.object({
91
+ server_seed: zod_1.z.string().min(1).max(256).describe('The revealed server seed'),
92
+ client_seed: zod_1.z.string().min(1).max(128).describe('The client seed used for the roll'),
93
+ nonce: zod_1.z.number().int().min(0).describe('The nonce that roll consumed (the pre-increment value)'),
94
+ server_seed_hash: zod_1.z.string().regex(/^[0-9a-fA-F]{64}$/).optional().describe('The SHA-256 commitment shown before the roll. If given, it is checked against server_seed'),
95
+ claimed_result: zod_1.z.number().int().min(0).optional().describe('The roll or result the game reported. If given, it is compared to the recomputed value for result_type'),
96
+ result_type: zod_1.z.enum(['dice', 'roll']).optional().describe('Which mapping claimed_result uses: dice (0-9999, default) or roll (0-99999)'),
97
+ });
98
+ const SpendPolicyGetSchema = zod_1.z.object({
99
+ agent_id: zod_1.z.number().int().positive().optional().describe('Operator key only: the agent to read. An agent key reads its own'),
100
+ });
101
+ const limitField = (what) => zod_1.z.number().int().min(0).max(100_000_000).nullable().optional().describe(`${what}. 0 or null clears the limit`);
102
+ const SpendPolicySetSchema = zod_1.z.object({
103
+ agent_id: zod_1.z.number().int().positive().describe('Agent id from list_agents'),
104
+ per_tx_max_sats: limitField('Largest single spend in sats'),
105
+ daily_max_sats: limitField('Rolling 24 hour gross spend in sats'),
106
+ per_market_max_sats: limitField('Net stake on any one prediction market in sats'),
107
+ daily_loss_cap_sats: limitField('Responsible gambling: net gambling loss in 24 hours before bets and arena stop'),
108
+ cooldown_minutes: zod_1.z.number().int().min(0).max(10080).nullable().optional().describe('Responsible gambling: pause length once the loss cap is hit. 0 or null clears it'),
109
+ }).refine((p) => ['per_tx_max_sats', 'daily_max_sats', 'per_market_max_sats', 'daily_loss_cap_sats', 'cooldown_minutes'].some((k) => p[k] !== undefined), { message: 'Pass at least one limit to change' });
110
+ const PickemBrowseSchema = zod_1.z.object({
111
+ week: zod_1.z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe('Week key (YYYY-MM-DD, a Monday) from the weeks list. Default current week'),
112
+ });
113
+ const TournamentListSchema = zod_1.z.object({
114
+ game_type: zod_1.z.string().min(1).max(30).optional().describe('Filter by game type, for example slots, blackjack, dice'),
115
+ });
116
+ // ─── Tool definitions ───────────────────────────────────────────────────
117
+ const GATEWAY_KEYS = 'REQUIRES OPERATOR KEY.';
118
+ exports.AGENT_TOOLS = [
119
+ {
120
+ name: 'link_user',
121
+ description: 'Link this operator account to a Lightning Faucet website account using that account\'s recovery code, so the same person sees both. The link is permanent. merge_balance defaults to false; true also moves the operator balance into the website account and cannot be undone. confirm must be true. REQUIRES OPERATOR KEY.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ recovery_code: { type: 'string', description: 'Recovery code of the website account' },
126
+ merge_balance: { type: 'boolean', default: false, description: 'Also move the operator balance (irreversible). Default false' },
127
+ confirm: { type: 'boolean', enum: [true], description: 'Must be true' },
128
+ },
129
+ required: ['recovery_code', 'confirm'],
130
+ },
131
+ },
132
+ {
133
+ name: 'export_transactions',
134
+ description: 'Export this agent\'s transaction history as JSON or CSV for accounting, optionally for a date range. Read-only. Large exports are truncated to about 60k characters with truncated:true; narrow the dates and call again. REQUIRES AGENT KEY.',
135
+ inputSchema: {
136
+ type: 'object',
137
+ properties: {
138
+ format: { type: 'string', enum: ['json', 'csv'], default: 'json' },
139
+ start_date: { type: 'string', description: 'Inclusive start, YYYY-MM-DD' },
140
+ end_date: { type: 'string', description: 'Inclusive end, YYYY-MM-DD' },
141
+ include_pending: { type: 'boolean', default: false },
142
+ },
143
+ },
144
+ },
145
+ {
146
+ name: 'get_agent_analytics',
147
+ description: 'Spending and earning analytics for this agent over a window: total spent and received, transaction count, average payment, and top L402 destinations. Read-only. REQUIRES AGENT KEY.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: { period: { type: 'string', enum: ['24h', '7d', '30d', 'all'], default: '30d' } },
151
+ },
152
+ },
153
+ {
154
+ name: 'gateway_catalog',
155
+ description: 'Browse the public catalog of paid endpoints that sellers have published on the Lightning Faucet L402 gateway. Public, no key. Pair with pay_l402_api to buy one.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ category: { type: 'string' },
160
+ page: { type: 'integer', minimum: 1, default: 1 },
161
+ page_size: { type: 'integer', minimum: 1, maximum: 50, default: 20 },
162
+ },
163
+ },
164
+ },
165
+ {
166
+ name: 'gateway_probe',
167
+ description: 'Test that a URL you want to sell is reachable and returns something sensible, before gateway_register. Makes one request to the URL from our servers; private and localhost addresses are refused. Read-only on our side. ' + GATEWAY_KEYS,
168
+ inputSchema: {
169
+ type: 'object',
170
+ properties: {
171
+ upstream_url: { type: 'string' },
172
+ upstream_method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'], default: 'GET' },
173
+ upstream_headers: { type: 'object', additionalProperties: { type: 'string' } },
174
+ },
175
+ required: ['upstream_url'],
176
+ },
177
+ },
178
+ {
179
+ name: 'gateway_register',
180
+ description: 'Seller side: put your HTTP endpoint behind an L402 paywall on our gateway. Callers pay price_sats over Lightning per request and we proxy to upstream_url. The upstream is probed first and registration fails if it is unreachable. Registering does not spend funds. ' + GATEWAY_KEYS,
181
+ inputSchema: {
182
+ type: 'object',
183
+ properties: {
184
+ name: { type: 'string', maxLength: 100 },
185
+ description: { type: 'string', maxLength: 500 },
186
+ upstream_url: { type: 'string' },
187
+ upstream_method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'], default: 'GET' },
188
+ price_sats: { type: 'integer', minimum: 1 },
189
+ category: { type: 'string' },
190
+ upstream_headers: { type: 'object', additionalProperties: { type: 'string' } },
191
+ upstream_timeout_ms: { type: 'integer', minimum: 1000, maximum: 30000 },
192
+ },
193
+ required: ['name', 'upstream_url', 'price_sats'],
194
+ },
195
+ },
196
+ {
197
+ name: 'gateway_list',
198
+ description: 'Seller side: list your gateway endpoints with status, price and slug. Read-only. ' + GATEWAY_KEYS,
199
+ inputSchema: { type: 'object', properties: {} },
200
+ },
201
+ {
202
+ name: 'gateway_update',
203
+ description: 'Seller side: change the name, description, price or upstream of one of your gateway endpoints. Only the fields you pass change. ' + GATEWAY_KEYS,
204
+ inputSchema: {
205
+ type: 'object',
206
+ properties: {
207
+ endpoint_id: { type: 'integer', minimum: 1 },
208
+ name: { type: 'string' },
209
+ description: { type: 'string' },
210
+ price_sats: { type: 'integer', minimum: 1 },
211
+ upstream_url: { type: 'string' },
212
+ upstream_method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'] },
213
+ upstream_headers: { type: 'object', additionalProperties: { type: 'string' } },
214
+ upstream_timeout_ms: { type: 'integer', minimum: 1000, maximum: 30000 },
215
+ },
216
+ required: ['endpoint_id'],
217
+ },
218
+ },
219
+ {
220
+ name: 'gateway_pause',
221
+ description: 'Seller side: stop selling an endpoint without deleting it. Reversible with gateway_resume. ' + GATEWAY_KEYS,
222
+ inputSchema: { type: 'object', properties: { endpoint_id: { type: 'integer', minimum: 1 } }, required: ['endpoint_id'] },
223
+ },
224
+ {
225
+ name: 'gateway_resume',
226
+ description: 'Seller side: resume a paused endpoint. ' + GATEWAY_KEYS,
227
+ inputSchema: { type: 'object', properties: { endpoint_id: { type: 'integer', minimum: 1 } }, required: ['endpoint_id'] },
228
+ },
229
+ {
230
+ name: 'gateway_delete',
231
+ description: 'Seller side: permanently delete a gateway endpoint. Prefer gateway_pause if you may want it back. confirm must be true. ' + GATEWAY_KEYS,
232
+ inputSchema: {
233
+ type: 'object',
234
+ properties: { endpoint_id: { type: 'integer', minimum: 1 }, confirm: { type: 'boolean', enum: [true] } },
235
+ required: ['endpoint_id', 'confirm'],
236
+ },
237
+ },
238
+ {
239
+ name: 'gateway_stats',
240
+ description: 'Seller side: calls, revenue and recent activity for one of your gateway endpoints. Read-only. ' + GATEWAY_KEYS,
241
+ inputSchema: { type: 'object', properties: { endpoint_id: { type: 'integer', minimum: 1 } }, required: ['endpoint_id'] },
242
+ },
243
+ {
244
+ name: 'list_l402_apis',
245
+ description: 'Discover L402 APIs you can pay for with pay_l402_api. source=first_party lists Lightning Faucet\'s own priced endpoints with live prices; registry is the community directory (filter by category or search); discovered lists endpoints seen in real L402 payments; gateway lists seller endpoints on our gateway. Public, no key, read-only.',
246
+ inputSchema: {
247
+ type: 'object',
248
+ properties: {
249
+ source: { type: 'string', enum: ['first_party', 'registry', 'discovered', 'gateway'], default: 'registry' },
250
+ category: { type: 'string' },
251
+ search: { type: 'string' },
252
+ limit: { type: 'integer', minimum: 1, maximum: 50, default: 20 },
253
+ page: { type: 'integer', minimum: 1, default: 1 },
254
+ },
255
+ },
256
+ },
257
+ {
258
+ name: 'verify_fair',
259
+ description: 'Verify a provably fair result locally, with no network call and no key. Recomputes HMAC-SHA256(server_seed, client_seed + ":" + nonce), derives the roll (first 8 hex digits as an integer, modulo 100000, then modulo 10000 for the 0-9999 dice range), optionally checks the SHA-256 commitment you saw before the roll (server_seed_hash) and compares your claimed_result. Works for Agent Arena dice and Lightning Faucet casino dice; returns the full hash so other games can apply their own mapping. Use the nonce the roll consumed, not the next one.',
260
+ inputSchema: {
261
+ type: 'object',
262
+ properties: {
263
+ server_seed: { type: 'string', description: 'Revealed server seed' },
264
+ client_seed: { type: 'string' },
265
+ nonce: { type: 'integer', minimum: 0, description: 'Nonce the roll consumed' },
266
+ server_seed_hash: { type: 'string', description: 'Optional SHA-256 commitment to check' },
267
+ claimed_result: { type: 'integer', minimum: 0, description: 'Optional reported roll to compare' },
268
+ result_type: { type: 'string', enum: ['dice', 'roll'], default: 'dice', description: 'Mapping claimed_result uses: dice (0-9999) or roll (0-99999)' },
269
+ },
270
+ required: ['server_seed', 'client_seed', 'nonce'],
271
+ },
272
+ },
273
+ {
274
+ name: 'get_spend_policy',
275
+ description: 'Read the spend limits on an agent: per-transaction, daily, per-market and responsible-gambling (daily loss cap, cooldown) limits, any pending loosening and an active cooldown. null means no limit. New agents start with conservative defaults. When a limit blocks a spend the error is typed (per_tx_cap_exceeded, daily_cap_exceeded, per_market_cap_exceeded, loss_cap_reached, cooldown_active) with the cap and what is left. Read-only. Agent key reads its own; operator key passes agent_id.',
276
+ inputSchema: {
277
+ type: 'object',
278
+ properties: { agent_id: { type: 'integer', minimum: 1, description: 'Operator key only' } },
279
+ },
280
+ },
281
+ {
282
+ name: 'set_spend_policy',
283
+ description: 'Set spend limits on one of your agents. Pass only the fields to change; 0 or null clears a limit. Tightening applies at once. Loosening a responsible-gambling limit (raising daily_loss_cap_sats, shortening or clearing cooldown_minutes) takes effect after 24 hours and is returned as pending_changes. Only the operator can set limits: an agent cannot raise its own. REQUIRES OPERATOR KEY.',
284
+ inputSchema: {
285
+ type: 'object',
286
+ properties: {
287
+ agent_id: { type: 'integer', minimum: 1 },
288
+ per_tx_max_sats: { type: ['integer', 'null'], minimum: 0, description: 'Largest single spend' },
289
+ daily_max_sats: { type: ['integer', 'null'], minimum: 0, description: 'Rolling 24h gross spend' },
290
+ per_market_max_sats: { type: ['integer', 'null'], minimum: 0, description: 'Net stake on one prediction market' },
291
+ daily_loss_cap_sats: { type: ['integer', 'null'], minimum: 0, description: 'Net gambling loss in 24h before bets and arena stop' },
292
+ cooldown_minutes: { type: ['integer', 'null'], minimum: 0, maximum: 10080, description: 'Pause once the loss cap is hit' },
293
+ },
294
+ required: ['agent_id'],
295
+ },
296
+ },
297
+ {
298
+ name: 'pickem_browse',
299
+ description: 'Weekly Pick\'em (free NFL and college football contest with bonus sat prizes): this week\'s slate, lock times, results, prize ladder and standings. Public and read-only. Entering requires a person\'s website account at https://lightningfaucet.com/pickem/, so there is no agent entry tool; tell your human about it, or forecast the same games on prediction markets with prediction_markets.',
300
+ inputSchema: {
301
+ type: 'object',
302
+ properties: { week: { type: 'string', description: 'Week key YYYY-MM-DD from the weeks list. Default current week' } },
303
+ },
304
+ },
305
+ {
306
+ name: 'tournament_list',
307
+ description: 'List the human casino tournaments running now (slots, blackjack, dice and more): buy-in, prize pool, schedule, leaderboard. Public and read-only. These are for people with website accounts; agents compete in the Agent Arena instead (arena_list, arena_join).',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: { game_type: { type: 'string', description: 'Filter, for example slots' } },
311
+ },
312
+ },
313
+ ];
314
+ exports.AGENT_TOOL_NAMES = new Set(exports.AGENT_TOOLS.map((t) => t.name));
315
+ // ─── Helpers ────────────────────────────────────────────────────────────
316
+ async function platformRead(action, data = {}) {
317
+ const res = await (0, lightning_faucet_js_1.fetchWithTimeout)(PLATFORM_API_URL, {
318
+ method: 'POST',
319
+ headers: { 'Content-Type': 'application/json' },
320
+ body: JSON.stringify({ action, ...data }),
321
+ });
322
+ if (!res.ok)
323
+ throw new Error(`Request failed (HTTP ${res.status})`);
324
+ const json = (await res.json());
325
+ if (json.success === false)
326
+ throw new lightning_faucet_js_1.ApiError(String(json.error ?? 'Unknown API error'), json);
327
+ return json;
328
+ }
329
+ /** Pure, exported for tests. */
330
+ function verifyFair(p) {
331
+ const hash = (0, node_crypto_1.createHmac)('sha256', p.server_seed).update(`${p.client_seed}:${p.nonce}`).digest('hex');
332
+ const roll = parseInt(hash.slice(0, 8), 16) % 100000;
333
+ const dice = roll % 10000;
334
+ const out = {
335
+ success: true,
336
+ hmac_sha256: hash,
337
+ roll_0_99999: roll,
338
+ dice_0_9999: dice,
339
+ formula: 'hash = HMAC_SHA256(server_seed, client_seed + ":" + nonce); roll = int(hash[0:8], 16) % 100000; dice = roll % 10000',
340
+ };
341
+ if (p.server_seed_hash) {
342
+ const commit = (0, node_crypto_1.createHash)('sha256').update(p.server_seed).digest('hex');
343
+ out.commitment_sha256 = commit;
344
+ out.commitment_matches = commit.toLowerCase() === p.server_seed_hash.toLowerCase();
345
+ }
346
+ if (p.claimed_result !== undefined) {
347
+ out.claimed_result = p.claimed_result;
348
+ const resultType = p.result_type ?? 'dice';
349
+ out.result_type = resultType;
350
+ out.result_matches = p.claimed_result === (resultType === 'roll' ? roll : dice);
351
+ }
352
+ const checks = [out.commitment_matches, out.result_matches].filter((v) => v !== undefined);
353
+ out.verified = checks.length > 0 ? checks.every(Boolean) : null;
354
+ if (checks.length === 0)
355
+ out.note = 'Pass server_seed_hash and/or claimed_result to get a verified true/false.';
356
+ return out;
357
+ }
358
+ function summarizeTournament(t) {
359
+ const keep = ['id', 'name', 'tier_label', 'game_type', 'schedule', 'status', 'entrants', 'start_at', 'end_at', 'buy_in_sats', 'spins_per_entry', 'prize_pool_sats', 'guarantee_sats', 'prize_structure', 'entry_count', 'player_count', 'time_remaining_seconds'];
360
+ const out = {};
361
+ for (const k of keep)
362
+ if (t[k] !== undefined)
363
+ out[k] = t[k];
364
+ if (Array.isArray(t.leaderboard))
365
+ out.top_5 = t.leaderboard.slice(0, 5).map((r) => ({ rank: r.rank, player: r.player, score: r.score }));
366
+ return out;
367
+ }
368
+ /** Agent-scoped actions answer "Unknown operator action" to an operator key; say what to do instead. */
369
+ async function agentOnly(tool, run) {
370
+ try {
371
+ return await run();
372
+ }
373
+ catch (e) {
374
+ if (e instanceof lightning_faucet_js_1.ApiError && /unknown operator action/i.test(e.message)) {
375
+ throw new lightning_faucet_js_1.ApiError('requires_agent_key', {
376
+ success: false,
377
+ error: 'requires_agent_key',
378
+ hint: `${tool} runs as one agent. You are using an operator key: use set_agent_credentials with that agent's key (agent_...), then call ${tool} again.`,
379
+ });
380
+ }
381
+ throw e;
382
+ }
383
+ }
384
+ // ─── Dispatch ───────────────────────────────────────────────────────────
385
+ /** Returns a result for tools owned by this module, or null so the caller continues. */
386
+ async function handleAgentTool(name, args, session) {
387
+ if (!exports.AGENT_TOOL_NAMES.has(name))
388
+ return null;
389
+ const a = args ?? {};
390
+ switch (name) {
391
+ case 'link_user': {
392
+ const p = LinkUserSchema.parse(a);
393
+ return ok(await session.requireClient().rawAction('link_user', { recovery_code: p.recovery_code, merge_balance: p.merge_balance }));
394
+ }
395
+ case 'export_transactions': {
396
+ const p = ExportTransactionsSchema.parse(a);
397
+ const data = { format: p.format, include_pending: p.include_pending };
398
+ if (p.start_date)
399
+ data.start_date = p.start_date;
400
+ if (p.end_date)
401
+ data.end_date = p.end_date;
402
+ const r = await agentOnly('export_transactions', () => session.requireClient().rawAction('export_transactions', data));
403
+ // Measure the pretty-printed form, which is what ok() actually returns.
404
+ const size = (v) => JSON.stringify(v, null, 2).length;
405
+ if (typeof r.csv === 'string') {
406
+ const csv = r.csv;
407
+ const cutCsv = (n) => ({ ...r, csv: csv.slice(0, n), truncated: true, next: 'Output was cut. Narrow start_date and end_date and call again.' });
408
+ if (size(r) <= MAX_TEXT_CHARS)
409
+ return ok({ ...r, truncated: false });
410
+ // JSON escaping inflates quotes/newlines, so shrink until the serialized response fits.
411
+ let n = Math.min(csv.length, MAX_TEXT_CHARS);
412
+ while (n > 0 && size(cutCsv(n)) > MAX_TEXT_CHARS)
413
+ n = Math.floor(n * 0.8);
414
+ return ok(cutCsv(n));
415
+ }
416
+ if (size(r) > MAX_TEXT_CHARS && Array.isArray(r.transactions)) {
417
+ const cut = (rows) => ({ ...r, transactions: rows, truncated: true, returned: rows.length, next: 'Output was cut. Narrow start_date and end_date and call again.' });
418
+ let rows = r.transactions;
419
+ while (rows.length > 1 && size(cut(rows)) > MAX_TEXT_CHARS)
420
+ rows = rows.slice(0, Math.ceil(rows.length / 2));
421
+ return ok(cut(rows));
422
+ }
423
+ return ok(r);
424
+ }
425
+ case 'get_agent_analytics': {
426
+ const p = AgentAnalyticsSchema.parse(a);
427
+ return ok(await agentOnly('get_agent_analytics', () => session.requireClient().rawAction('get_agent_analytics', { period: p.period })));
428
+ }
429
+ case 'gateway_catalog': {
430
+ const p = GatewayCatalogSchema.parse(a);
431
+ const c = session.getClient();
432
+ const data = { ...p };
433
+ return ok(c ? await c.rawAction('gateway_catalog', data) : await (0, lightning_faucet_js_1.getPublicAction)('gateway_catalog', data));
434
+ }
435
+ case 'gateway_probe': return ok(await session.requireClient().rawAction('gateway_probe', GatewayProbeSchema.parse(a)));
436
+ case 'gateway_register': return ok(await session.requireClient().rawAction('gateway_register', GatewayRegisterSchema.parse(a)));
437
+ case 'gateway_list': return ok(await session.requireClient().rawAction('gateway_list'));
438
+ case 'gateway_update': return ok(await session.requireClient().rawAction('gateway_update', GatewayUpdateSchema.parse(a)));
439
+ case 'gateway_pause': return ok(await session.requireClient().rawAction('gateway_pause', GatewayIdSchema.parse(a)));
440
+ case 'gateway_resume': return ok(await session.requireClient().rawAction('gateway_resume', GatewayIdSchema.parse(a)));
441
+ case 'gateway_delete': {
442
+ const p = GatewayDeleteSchema.parse(a);
443
+ return ok(await session.requireClient().rawAction('gateway_delete', { endpoint_id: p.endpoint_id }));
444
+ }
445
+ case 'gateway_stats': return ok(await session.requireClient().rawAction('gateway_stats', GatewayIdSchema.parse(a)));
446
+ case 'list_l402_apis': {
447
+ const p = ListL402Schema.parse(a);
448
+ if (p.source === 'first_party') {
449
+ const res = await (0, lightning_faucet_js_1.fetchWithTimeout)(L402_WELL_KNOWN_URL, { method: 'GET', headers: { Accept: 'application/json' } });
450
+ if (!res.ok)
451
+ return typedError('upstream_unavailable', `Could not read ${L402_WELL_KNOWN_URL} (HTTP ${res.status}). Try again shortly.`);
452
+ return ok({ success: true, source: 'first_party', source_url: L402_WELL_KNOWN_URL, discovery: await res.json() });
453
+ }
454
+ const act = p.source === 'registry' ? 'registry_list' : p.source === 'discovered' ? 'get_l402_discovered' : 'gateway_catalog';
455
+ const data = p.source === 'discovered' ? { limit: p.limit, page: p.page } : { page: p.page, page_size: p.limit };
456
+ if (p.category && p.source !== 'discovered')
457
+ data.category = p.category;
458
+ if (p.search && p.source === 'registry')
459
+ data.search = p.search;
460
+ const c = session.getClient();
461
+ const r = c ? await c.rawAction(act, data) : await (0, lightning_faucet_js_1.getPublicAction)(act, data);
462
+ return ok({ source: p.source, ...r, next: 'Call an entry with pay_l402_api (set max_payment_sats).' });
463
+ }
464
+ case 'get_spend_policy': {
465
+ const p = SpendPolicyGetSchema.parse(a);
466
+ return ok(await session.requireClient().rawAction('get_spend_policy', p.agent_id ? { agent_id: p.agent_id } : {}));
467
+ }
468
+ case 'set_spend_policy': {
469
+ const p = SpendPolicySetSchema.parse(a);
470
+ const data = {};
471
+ for (const [k, v] of Object.entries(p))
472
+ if (v !== undefined)
473
+ data[k] = v;
474
+ return ok(await session.requireClient().rawAction('set_spend_policy', data));
475
+ }
476
+ case 'verify_fair': return ok(verifyFair(VerifyFairSchema.parse(a)));
477
+ case 'pickem_browse': {
478
+ const p = PickemBrowseSchema.parse(a);
479
+ const r = await platformRead('pickem_state', p.week ? { week: p.week } : {});
480
+ return ok({ ...r, enter_at: 'https://lightningfaucet.com/pickem/', agent_entry: 'not available: entry needs a person\'s website account. Forecast the same games with prediction_markets.' });
481
+ }
482
+ case 'tournament_list': {
483
+ const p = TournamentListSchema.parse(a);
484
+ const r = await platformRead('get_active_tournaments');
485
+ let list = Array.isArray(r.tournaments) ? r.tournaments : [];
486
+ if (p.game_type)
487
+ list = list.filter((t) => t.game_type === p.game_type);
488
+ return ok({ success: true, count: list.length, tournaments: list.map(summarizeTournament), join_at: 'https://lightningfaucet.com/casino/', agent_tournaments: 'Use arena_list and arena_join for tournaments agents can enter.' });
489
+ }
490
+ default:
491
+ return null;
492
+ }
493
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};