apow-cli 0.2.0 → 0.3.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/.env.example CHANGED
@@ -1,7 +1,8 @@
1
1
  # Wallet
2
2
  PRIVATE_KEY=
3
3
 
4
- # RPC
4
+ # RPC — The public default works but is unreliable for sustained mining.
5
+ # We strongly recommend a free Alchemy endpoint: https://www.alchemy.com/ (no credit card)
5
6
  RPC_URL=https://mainnet.base.org
6
7
 
7
8
  # LLM Provider (openai / anthropic / gemini / ollama / claude-code / codex)
package/README.md CHANGED
@@ -16,6 +16,16 @@ Or run directly:
16
16
  npx apow-cli
17
17
  ```
18
18
 
19
+ ## Important: RPC Endpoint
20
+
21
+ > **The default public RPC (`mainnet.base.org`) is unreliable for mining.** It rate-limits aggressively and will cause frequent transaction failures. **We strongly recommend getting a free Alchemy endpoint** before you start:
22
+ >
23
+ > 1. Sign up at [alchemy.com](https://www.alchemy.com/) (free, no credit card)
24
+ > 2. Create an app → Chain: **Base** → Network: **Base Mainnet**
25
+ > 3. Copy the HTTPS URL → set `RPC_URL` in your `.env`
26
+ >
27
+ > Alchemy's free tier gives you 300M compute units/month — more than enough for mining. See [RPC Recommendations](skill.md#rpc-recommendations) for other free options.
28
+
19
29
  ## For AI Agents
20
30
 
21
31
  Any AI agent can go from zero to actively mining in 5 steps — no human interaction except funding the wallet.
@@ -28,7 +38,7 @@ npx apow-cli wallet new
28
38
  # 2. Write .env (no interactive prompts)
29
39
  cat > .env << 'EOF'
30
40
  PRIVATE_KEY=0x<from step 1>
31
- RPC_URL=https://mainnet.base.org
41
+ RPC_URL=https://mainnet.base.org # UNRELIABLE — get a free Alchemy URL (see above)
32
42
  LLM_PROVIDER=openai
33
43
  LLM_MODEL=gpt-4o-mini
34
44
  LLM_API_KEY=<your key>
@@ -97,7 +107,7 @@ Create a `.env` file or use `apow setup`:
97
107
 
98
108
  ```bash
99
109
  PRIVATE_KEY=0x... # Your wallet private key
100
- RPC_URL=https://mainnet.base.org
110
+ RPC_URL=https://mainnet.base.org # UNRELIABLE — strongly recommend a free Alchemy URL instead (see above)
101
111
  LLM_PROVIDER=openai # openai | anthropic | gemini | ollama | claude-code | codex
102
112
  LLM_MODEL=gpt-4o-mini
103
113
  LLM_API_KEY=sk-...
@@ -182,5 +182,22 @@
182
182
  "type": "uint256"
183
183
  }
184
184
  ]
185
+ },
186
+ {
187
+ "type": "function",
188
+ "name": "tokenURI",
189
+ "stateMutability": "view",
190
+ "inputs": [
191
+ {
192
+ "name": "tokenId",
193
+ "type": "uint256"
194
+ }
195
+ ],
196
+ "outputs": [
197
+ {
198
+ "name": "",
199
+ "type": "string"
200
+ }
201
+ ]
185
202
  }
186
203
  ]
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDashboardHtml = getDashboardHtml;
4
+ function getDashboardHtml() {
5
+ return `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10
+ <title>APoW Dashboard</title>
11
+ <style>
12
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
13
+ :root{
14
+ --bg:#0a0a0a;--card:#141414;--card-border:#161616;
15
+ --text:#e5e5e5;--text-dim:#737373;--accent:#0052FF;
16
+ --rarity-common:#a1a1aa;--rarity-uncommon:#4ade80;
17
+ --rarity-rare:#60a5fa;--rarity-epic:#a78bfa;--rarity-mythic:#fbbf24;
18
+ }
19
+ body{background:var(--bg);color:var(--text);font-family:SFMono-Regular,'SF Mono',Menlo,Consolas,'Liberation Mono',monospace;font-size:13px;line-height:1.4}
20
+ a{color:inherit;text-decoration:none}
21
+ .container{min-height:100vh;padding:12px}
22
+ /* Header */
23
+ .header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
24
+ .header h1{font-size:14px;font-weight:700;letter-spacing:.05em}
25
+ .status{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-dim)}
26
+ .pulse{display:inline-block;height:6px;width:6px;border-radius:50%;background:var(--accent);animation:pulse 1.5s infinite}
27
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
28
+ /* Banners */
29
+ .rpc-warning{border-radius:6px;border:1px solid #7c2d12;background:rgba(69,26,3,.3);padding:8px 12px;font-size:12px;color:#fdba74;margin-bottom:12px}
30
+ .rpc-warning a{text-decoration:underline}
31
+ .rpc-warning a:hover{color:#fed7aa}
32
+ .error-banner{border-radius:6px;border:1px solid #7f1d1d;background:rgba(69,10,10,.3);padding:8px 12px;font-size:12px;color:#f87171;margin-bottom:12px}
33
+ /* Fleet tabs */
34
+ .fleet-tabs{display:flex;align-items:center;gap:4px;margin-bottom:12px;overflow-x:auto}
35
+ .fleet-tab{padding:4px 12px;font-size:12px;border-radius:4px;cursor:pointer;white-space:nowrap;border:1px solid transparent;background:var(--card);color:var(--text-dim);transition:all .15s}
36
+ .fleet-tab:hover{color:var(--text)}
37
+ .fleet-tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}
38
+ /* Stats grid */
39
+ .stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1px;background:var(--card-border);border-radius:8px;overflow:hidden;margin-bottom:12px}
40
+ @media(min-width:640px){.stats-grid{grid-template-columns:repeat(5,1fr)}}
41
+ @media(min-width:1024px){.stats-grid{grid-template-columns:repeat(12,1fr)}}
42
+ .stat-cell{background:var(--card);padding:8px 12px}
43
+ .stat-label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em}
44
+ .stat-value{font-size:14px;font-weight:600}
45
+ .stat-value.highlight{color:var(--accent)}
46
+ .stat-sub{font-size:10px;color:var(--text-dim)}
47
+ /* Wallet grid */
48
+ .wallet-grid{display:grid;grid-template-columns:1fr;gap:8px}
49
+ @media(min-width:640px){.wallet-grid{grid-template-columns:repeat(2,1fr)}}
50
+ @media(min-width:1024px){.wallet-grid{grid-template-columns:repeat(3,1fr)}}
51
+ @media(min-width:1280px){.wallet-grid{grid-template-columns:repeat(4,1fr)}}
52
+ .wallet-card{border-radius:6px;border:1px solid var(--card-border);background:var(--card);overflow:hidden}
53
+ .wallet-card.active{border-color:var(--accent)}
54
+ .wallet-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid var(--card-border)}
55
+ .wallet-addr{font-size:11px;font-weight:500;color:var(--text-dim);transition:color .15s}
56
+ .wallet-addr:hover{color:var(--accent)}
57
+ .wallet-stats{display:flex;align-items:center;gap:12px;font-size:11px}
58
+ .wallet-stats .hp{color:var(--rarity-uncommon)}
59
+ .wallet-stats .dim{color:var(--text-dim)}
60
+ .wallet-stats .agent-hl{color:var(--accent)}
61
+ .miners-wrap{display:flex;flex-wrap:wrap;gap:8px;padding:8px 10px}
62
+ .miner-thumb{display:flex;align-items:center;gap:4px;text-decoration:none}
63
+ .miner-thumb:hover .miner-img{box-shadow:0 0 0 1px var(--accent)}
64
+ .miner-thumb:hover .miner-id{color:var(--accent)}
65
+ .miner-img{height:40px;width:40px;border-radius:4px;transition:box-shadow .15s}
66
+ .miner-placeholder{height:40px;width:40px;border-radius:4px;background:rgba(255,255,255,.05);animation:pulse 1.5s infinite}
67
+ .miner-id{font-size:9px;color:var(--text-dim);transition:color .15s}
68
+ .no-miners{padding:8px 10px;font-size:10px;color:var(--text-dim)}
69
+ /* Empty state */
70
+ .empty-state{border-radius:6px;border:1px solid var(--card-border);background:var(--card);padding:16px 20px;font-size:12px;color:var(--text-dim)}
71
+ .empty-state h2{font-size:14px;color:var(--text);text-align:center;margin-bottom:12px}
72
+ .empty-state code{color:var(--accent)}
73
+ .empty-state .cmds{margin:8px 0 0 8px;line-height:2}
74
+ .empty-state .hint{margin-top:12px;font-size:10px}
75
+ /* Loading */
76
+ .loading{text-align:center;padding:80px 0;color:var(--text-dim)}
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <div class="container">
81
+ <div class="header">
82
+ <h1>APoW DASHBOARD</h1>
83
+ <div class="status"><span class="pulse" id="statusDot"></span><span id="statusText">Loading...</span></div>
84
+ </div>
85
+ <div id="rpcWarning" class="rpc-warning" style="display:none">
86
+ Using public Base RPC (mainnet.base.org) — this is unreliable for dashboards with many wallets.
87
+ Get a free dedicated endpoint at <a href="https://www.alchemy.com/base" target="_blank" rel="noopener noreferrer">alchemy.com</a> for reliable data.
88
+ </div>
89
+ <div id="errorBanner" class="error-banner" style="display:none">Failed to fetch data. Check RPC connection.</div>
90
+ <div id="fleetTabs" class="fleet-tabs" style="display:none"></div>
91
+ <div id="statsGrid" class="stats-grid" style="display:none"></div>
92
+ <div id="walletGrid" class="wallet-grid"></div>
93
+ <div id="emptyState" class="empty-state" style="display:none">
94
+ <h2>No wallets detected.</h2>
95
+ <p>To add wallets:</p>
96
+ <div class="cmds">
97
+ <div><code>apow dashboard add &lt;address&gt;</code> <span>— add a specific address</span></div>
98
+ <div><code>apow dashboard scan</code> <span>— auto-detect from wallet files in current dir</span></div>
99
+ </div>
100
+ <p class="hint">Wallets are also auto-detected from your .env PRIVATE_KEY on dashboard start.</p>
101
+ </div>
102
+ <div id="loading" class="loading">Loading...</div>
103
+ </div>
104
+ <script>
105
+ (function(){
106
+ var activeFleet = 'All';
107
+ var balanceHistory = [];
108
+ var prevMines = {};
109
+ var activeWallets = {};
110
+ var lastSeen = {};
111
+
112
+ function fetchJson(url) {
113
+ return fetch(url).then(function(r) {
114
+ if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
115
+ return r.json();
116
+ });
117
+ }
118
+
119
+ function fmt(n, d) { return Number(n).toLocaleString(undefined, { maximumFractionDigits: d !== undefined ? d : 1 }); }
120
+ function fmtFixed(n, d) { return Number(n).toFixed(d); }
121
+
122
+ function statCellHtml(label, value, opts) {
123
+ opts = opts || {};
124
+ var cls = 'stat-value' + (opts.highlight ? ' highlight' : '');
125
+ var sub = opts.sub ? '<div class="stat-sub">' + opts.sub + '</div>' : '';
126
+ return '<div class="stat-cell"><div class="stat-label">' + label + '</div><div class="' + cls + '">' + value + '</div>' + sub + '</div>';
127
+ }
128
+
129
+ function shortAddr(addr) { return addr.slice(0, 6) + '...' + addr.slice(-4); }
130
+
131
+ function escapeHtml(s) {
132
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
133
+ }
134
+
135
+ function updateMiningRate(totalAgent) {
136
+ if (totalAgent <= 0) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
137
+ var now = Date.now();
138
+ var h = balanceHistory;
139
+ if (h.length === 0 || h[h.length - 1].agent !== totalAgent) {
140
+ h.push({ agent: totalAgent, timestamp: now });
141
+ }
142
+ if (h.length > 120) h.splice(0, h.length - 120);
143
+ if (h.length < 2) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
144
+ var first = h[0], last = h[h.length - 1];
145
+ var elapsedMin = (last.timestamp - first.timestamp) / 60000;
146
+ if (elapsedMin < 0.5) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
147
+ var gained = last.agent - first.agent;
148
+ var perMin = gained / elapsedMin;
149
+ return { perMin: perMin, perHour: perMin * 60, sessionGain: gained, sessionMinutes: Math.floor(elapsedMin) };
150
+ }
151
+
152
+ function updateActiveWallets(wallets) {
153
+ if (!wallets) return;
154
+ var now = Date.now();
155
+ for (var i = 0; i < wallets.length; i++) {
156
+ var w = wallets[i];
157
+ var totalMines = 0;
158
+ for (var j = 0; j < w.miners.length; j++) totalMines += Number(w.miners[j].mineCount);
159
+ var prev = prevMines[w.address];
160
+ if (prev !== undefined && totalMines > prev) {
161
+ activeWallets[w.address] = true;
162
+ lastSeen[w.address] = now;
163
+ }
164
+ if (lastSeen[w.address] && now - lastSeen[w.address] > 300000) {
165
+ delete activeWallets[w.address];
166
+ }
167
+ prevMines[w.address] = totalMines;
168
+ }
169
+ }
170
+
171
+ function renderFleetTabs(fleets) {
172
+ var el = document.getElementById('fleetTabs');
173
+ if (!fleets || fleets.length <= 1) { el.style.display = 'none'; return; }
174
+ el.style.display = 'flex';
175
+ var totalCount = 0;
176
+ for (var i = 0; i < fleets.length; i++) totalCount += fleets[i].walletCount;
177
+ var html = '<div class="fleet-tab' + (activeFleet === 'All' ? ' active' : '') + '" data-fleet="All">All (' + totalCount + ')</div>';
178
+ for (var i = 0; i < fleets.length; i++) {
179
+ var f = fleets[i];
180
+ html += '<div class="fleet-tab' + (activeFleet === f.name ? ' active' : '') + '" data-fleet="' + escapeHtml(f.name) + '">' + escapeHtml(f.name) + ' (' + f.walletCount + ')</div>';
181
+ }
182
+ el.innerHTML = html;
183
+ var tabs = el.querySelectorAll('.fleet-tab');
184
+ for (var t = 0; t < tabs.length; t++) {
185
+ tabs[t].addEventListener('click', function() {
186
+ activeFleet = this.getAttribute('data-fleet');
187
+ refresh();
188
+ });
189
+ }
190
+ }
191
+
192
+ function renderStats(wallets, network) {
193
+ var el = document.getElementById('statsGrid');
194
+ if (!wallets && !network) { el.style.display = 'none'; return; }
195
+ el.style.display = 'grid';
196
+ var totalAgent = 0, totalEth = 0, totalMiners = 0, totalHashpower = 0;
197
+ if (wallets) {
198
+ for (var i = 0; i < wallets.length; i++) {
199
+ totalAgent += Number(wallets[i].agentBalance);
200
+ totalEth += Number(wallets[i].ethBalance);
201
+ totalMiners += wallets[i].miners.length;
202
+ for (var j = 0; j < wallets[i].miners.length; j++) totalHashpower += wallets[i].miners[j].hashpower;
203
+ }
204
+ }
205
+ var rate = updateMiningRate(totalAgent);
206
+ var html = '';
207
+ html += statCellHtml('TOTAL AGENT', fmt(totalAgent, 1), { highlight: true });
208
+ html += statCellHtml('TOTAL ETH', fmtFixed(totalEth, 4));
209
+ html += statCellHtml('WALLETS', wallets ? String(wallets.length) : '\\u2014');
210
+ html += statCellHtml('MINERS', String(totalMiners));
211
+ html += statCellHtml('HASHPOWER', fmtFixed(totalHashpower / 100, 1) + 'x');
212
+ html += statCellHtml('AGENT/MIN', rate.perMin !== null ? fmtFixed(rate.perMin, 2) : '\\u2014', { highlight: rate.perMin !== null && rate.perMin > 0 });
213
+ html += statCellHtml('AGENT/HR', rate.perHour !== null ? fmtFixed(rate.perHour, 1) : '\\u2014', {
214
+ highlight: rate.perHour !== null && rate.perHour > 0,
215
+ sub: rate.sessionGain !== null ? '+' + fmtFixed(rate.sessionGain, 1) + ' in ' + rate.sessionMinutes + 'm' : undefined
216
+ });
217
+ if (network) {
218
+ html += statCellHtml('ERA', String(network.era), { sub: fmt(network.minesUntilNextEra, 0) + ' to next' });
219
+ html += statCellHtml('BASE REWARD', fmtFixed(network.baseReward, 2) + ' AGENT/mine');
220
+ html += statCellHtml('SUPPLY', fmtFixed(network.supplyPct, 2) + '%');
221
+ html += statCellHtml('DIFFICULTY', String(network.difficulty));
222
+ html += statCellHtml('NETWORK MINES', fmt(network.totalMines, 0));
223
+ }
224
+ el.innerHTML = html;
225
+ }
226
+
227
+ function renderWallets(wallets) {
228
+ var grid = document.getElementById('walletGrid');
229
+ var empty = document.getElementById('emptyState');
230
+ if (!wallets || wallets.length === 0) {
231
+ grid.innerHTML = '';
232
+ if (wallets) empty.style.display = 'block';
233
+ return;
234
+ }
235
+ empty.style.display = 'none';
236
+ wallets = wallets.slice().sort(function(a, b) { return Number(b.agentBalance) - Number(a.agentBalance); });
237
+ var html = '';
238
+ for (var i = 0; i < wallets.length; i++) {
239
+ var w = wallets[i];
240
+ var isActive = !!activeWallets[w.address];
241
+ var hasAgent = Number(w.agentBalance) > 0;
242
+ var walletHp = 0;
243
+ for (var j = 0; j < w.miners.length; j++) walletHp += w.miners[j].hashpower;
244
+ html += '<div class="wallet-card' + (isActive ? ' active' : '') + '">';
245
+ html += '<div class="wallet-header">';
246
+ html += '<a class="wallet-addr" href="https://basescan.org/address/' + w.address + '" target="_blank" rel="noopener noreferrer">' + shortAddr(w.address) + '</a>';
247
+ html += '<div class="wallet-stats">';
248
+ if (w.miners.length > 0) html += '<span class="hp">' + fmtFixed(walletHp / 100, 1) + 'x</span>';
249
+ html += '<span><span class="dim">E </span>' + fmtFixed(Number(w.ethBalance), 4) + '</span>';
250
+ html += '<span' + (hasAgent ? ' class="agent-hl"' : '') + '><span class="dim">A </span>' + fmt(Number(w.agentBalance), 1) + '</span>';
251
+ html += '</div></div>';
252
+ if (w.miners.length > 0) {
253
+ html += '<div class="miners-wrap">';
254
+ for (var j = 0; j < w.miners.length; j++) {
255
+ var m = w.miners[j];
256
+ var osUrl = 'https://opensea.io/item/base/0xb7cad3ca5f2bd8aec2eb67d6e8d448099b3bc03d/' + m.tokenId;
257
+ html += '<a class="miner-thumb" href="' + osUrl + '" target="_blank" rel="noopener noreferrer">';
258
+ if (m.imageUri) {
259
+ html += '<img class="miner-img" src="' + escapeHtml(m.imageUri) + '" alt="#' + m.tokenId + '">';
260
+ } else {
261
+ html += '<div class="miner-placeholder"></div>';
262
+ }
263
+ html += '<span class="miner-id">#' + m.tokenId + '</span></a>';
264
+ }
265
+ html += '</div>';
266
+ } else {
267
+ html += '<div class="no-miners">No miners</div>';
268
+ }
269
+ html += '</div>';
270
+ }
271
+ grid.innerHTML = html;
272
+ }
273
+
274
+ var networkData = null;
275
+ var walletsData = null;
276
+ var hasError = false;
277
+
278
+ function setStatus(refreshing) {
279
+ document.getElementById('statusDot').style.display = refreshing ? 'inline-block' : 'none';
280
+ document.getElementById('statusText').textContent = refreshing ? 'Refreshing...' : 'Live';
281
+ }
282
+
283
+ function refresh() {
284
+ setStatus(true);
285
+ var fleetParam = encodeURIComponent(activeFleet);
286
+ Promise.all([
287
+ fetchJson('/api/network').catch(function(e) { return null; }),
288
+ fetchJson('/api/wallets?fleet=' + fleetParam).catch(function(e) { return null; }),
289
+ fetchJson('/api/fleets').catch(function(e) { return null; }),
290
+ fetchJson('/api/config').catch(function(e) { return null; })
291
+ ]).then(function(results) {
292
+ document.getElementById('loading').style.display = 'none';
293
+ var net = results[0], wal = results[1], fleets = results[2], cfg = results[3];
294
+ hasError = !net && !wal;
295
+ document.getElementById('errorBanner').style.display = hasError ? 'block' : 'none';
296
+ if (net) networkData = net;
297
+ if (wal && !wal.error) {
298
+ walletsData = wal;
299
+ updateActiveWallets(wal);
300
+ }
301
+ if (cfg) {
302
+ document.getElementById('rpcWarning').style.display = cfg.rpcIsDefault ? 'block' : 'none';
303
+ }
304
+ renderFleetTabs(fleets);
305
+ renderStats(walletsData, networkData);
306
+ renderWallets(walletsData);
307
+ setStatus(false);
308
+ }).catch(function() {
309
+ document.getElementById('loading').style.display = 'none';
310
+ document.getElementById('errorBanner').style.display = 'block';
311
+ setStatus(false);
312
+ });
313
+ }
314
+
315
+ refresh();
316
+ setInterval(refresh, 30000);
317
+ })();
318
+ </script>
319
+ </body>
320
+ </html>`;
321
+ }
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.startDashboardServer = startDashboardServer;
40
+ const http = __importStar(require("node:http"));
41
+ const node_fs_1 = require("node:fs");
42
+ const node_path_1 = require("node:path");
43
+ const viem_1 = require("viem");
44
+ const chains_1 = require("viem/chains");
45
+ const dashboard_html_1 = require("./dashboard-html");
46
+ const AgentCoin_json_1 = __importDefault(require("./abi/AgentCoin.json"));
47
+ const MiningAgent_json_1 = __importDefault(require("./abi/MiningAgent.json"));
48
+ const AgentCoinAbi = AgentCoin_json_1.default;
49
+ const MiningAgentAbi = MiningAgent_json_1.default;
50
+ const RARITY_LABELS = ["Common", "Uncommon", "Rare", "Epic", "Mythic"];
51
+ const ADDR_RE = /^0x[0-9a-fA-F]{40}$/;
52
+ const DEFAULT_RPC = "https://mainnet.base.org";
53
+ const FLEETS_PATH = (0, node_path_1.join)(process.env.HOME ?? "", ".apow", "fleets.json");
54
+ // --- Wallet / Fleet loading ---
55
+ function isAddress(s) {
56
+ return ADDR_RE.test(s);
57
+ }
58
+ function extractArray(path) {
59
+ const raw = (0, node_fs_1.readFileSync)(path, "utf8");
60
+ const data = JSON.parse(raw);
61
+ if (!Array.isArray(data))
62
+ return [];
63
+ return data.filter((a) => typeof a === "string" && isAddress(a));
64
+ }
65
+ function extractSolkek(path) {
66
+ const raw = (0, node_fs_1.readFileSync)(path, "utf8");
67
+ const data = JSON.parse(raw);
68
+ const addrs = [];
69
+ if (data.master?.address && isAddress(data.master.address)) {
70
+ addrs.push(data.master.address);
71
+ }
72
+ if (Array.isArray(data.miners)) {
73
+ for (const m of data.miners) {
74
+ if (m.address && isAddress(m.address)) {
75
+ addrs.push(m.address);
76
+ }
77
+ }
78
+ }
79
+ return addrs;
80
+ }
81
+ function extractRigdirs(dir) {
82
+ const addrs = [];
83
+ const entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ if (!entry.isDirectory() || !entry.name.startsWith("rig"))
86
+ continue;
87
+ const rigFiles = (0, node_fs_1.readdirSync)((0, node_path_1.join)(dir, entry.name));
88
+ for (const file of rigFiles) {
89
+ const match = file.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
90
+ if (match && isAddress(match[1])) {
91
+ addrs.push(match[1]);
92
+ }
93
+ }
94
+ }
95
+ return addrs;
96
+ }
97
+ function extractWalletfiles(dir) {
98
+ const addrs = [];
99
+ const files = (0, node_fs_1.readdirSync)(dir);
100
+ for (const file of files) {
101
+ const match = file.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
102
+ if (match && isAddress(match[1])) {
103
+ addrs.push(match[1]);
104
+ }
105
+ }
106
+ return addrs;
107
+ }
108
+ function getFleets(walletsPath) {
109
+ try {
110
+ const raw = (0, node_fs_1.readFileSync)(FLEETS_PATH, "utf8");
111
+ const configs = JSON.parse(raw);
112
+ return configs.map((cfg) => {
113
+ let addresses = [];
114
+ try {
115
+ switch (cfg.type) {
116
+ case "array":
117
+ addresses = extractArray(cfg.path);
118
+ break;
119
+ case "solkek":
120
+ addresses = extractSolkek(cfg.path);
121
+ break;
122
+ case "rigdirs":
123
+ addresses = extractRigdirs(cfg.path);
124
+ break;
125
+ case "walletfiles":
126
+ addresses = extractWalletfiles(cfg.path);
127
+ break;
128
+ }
129
+ }
130
+ catch {
131
+ // Skip broken fleet sources silently
132
+ }
133
+ return { name: cfg.name, addresses };
134
+ });
135
+ }
136
+ catch {
137
+ return [{ name: "Main", addresses: getWalletAddresses(walletsPath) }];
138
+ }
139
+ }
140
+ function getWalletAddresses(walletsPath) {
141
+ try {
142
+ const raw = (0, node_fs_1.readFileSync)(walletsPath, "utf8");
143
+ const data = JSON.parse(raw);
144
+ if (Array.isArray(data)) {
145
+ return data.filter((addr) => typeof addr === "string" && ADDR_RE.test(addr));
146
+ }
147
+ return [];
148
+ }
149
+ catch {
150
+ return [];
151
+ }
152
+ }
153
+ function getAddressesForFleet(fleetName, walletsPath) {
154
+ if (!fleetName || fleetName === "All") {
155
+ const fleets = getFleets(walletsPath);
156
+ const seen = new Set();
157
+ const all = [];
158
+ for (const f of fleets) {
159
+ for (const addr of f.addresses) {
160
+ const lower = addr.toLowerCase();
161
+ if (!seen.has(lower)) {
162
+ seen.add(lower);
163
+ all.push(addr);
164
+ }
165
+ }
166
+ }
167
+ return all;
168
+ }
169
+ const fleets = getFleets(walletsPath);
170
+ const fleet = fleets.find((f) => f.name === fleetName);
171
+ if (fleet)
172
+ return fleet.addresses;
173
+ return getWalletAddresses(walletsPath);
174
+ }
175
+ // --- Art parsing ---
176
+ function parseArtFromTokenUri(raw) {
177
+ if (!raw?.startsWith("data:application/json;base64,"))
178
+ return "";
179
+ try {
180
+ const json = JSON.parse(Buffer.from(raw.slice(29), "base64").toString("utf8"));
181
+ return json.image ?? "";
182
+ }
183
+ catch {
184
+ return "";
185
+ }
186
+ }
187
+ // --- Server ---
188
+ function startDashboardServer(opts) {
189
+ const { port, walletsPath, rpcUrl, miningAgentAddress, agentCoinAddress } = opts;
190
+ const publicClient = (0, viem_1.createPublicClient)({
191
+ chain: chains_1.base,
192
+ transport: (0, viem_1.http)(rpcUrl),
193
+ });
194
+ const artCache = new Map();
195
+ const htmlPage = (0, dashboard_html_1.getDashboardHtml)();
196
+ async function handleWallets(fleetParam) {
197
+ const addresses = getAddressesForFleet(fleetParam, walletsPath);
198
+ if (addresses.length === 0)
199
+ return "[]";
200
+ // Phase 1: ETH balance + AGENT balance + NFT count
201
+ const phase1Contracts = addresses.flatMap((addr) => [
202
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "balanceOf", args: [addr] },
203
+ { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "balanceOf", args: [addr] },
204
+ ]);
205
+ const [balances, multicallResults] = await Promise.all([
206
+ Promise.all(addresses.map((addr) => publicClient.getBalance({ address: addr }).catch(() => 0n))),
207
+ publicClient.multicall({ contracts: phase1Contracts }),
208
+ ]);
209
+ const walletInfos = addresses.map((addr, i) => ({
210
+ address: addr,
211
+ ethBalance: balances[i],
212
+ agentBalance: multicallResults[i * 2].result ?? 0n,
213
+ nftCount: Number(multicallResults[i * 2 + 1].result ?? 0n),
214
+ }));
215
+ // Phase 2: token IDs
216
+ const tokenIdContracts = [];
217
+ const tokenIdMap = [];
218
+ for (let wi = 0; wi < walletInfos.length; wi++) {
219
+ const info = walletInfos[wi];
220
+ for (let mi = 0; mi < info.nftCount; mi++) {
221
+ tokenIdContracts.push({
222
+ address: miningAgentAddress,
223
+ abi: MiningAgentAbi,
224
+ functionName: "tokenOfOwnerByIndex",
225
+ args: [info.address, BigInt(mi)],
226
+ });
227
+ tokenIdMap.push({ walletIdx: wi });
228
+ }
229
+ }
230
+ const tokenIds = [];
231
+ const validTokenMap = [];
232
+ if (tokenIdContracts.length > 0) {
233
+ const tokenIdResults = await publicClient.multicall({ contracts: tokenIdContracts });
234
+ for (let i = 0; i < tokenIdResults.length; i++) {
235
+ const r = tokenIdResults[i];
236
+ if (r.status === "success" && r.result != null) {
237
+ validTokenMap.push(i);
238
+ tokenIds.push(r.result);
239
+ }
240
+ }
241
+ }
242
+ // No miners — return early
243
+ if (tokenIds.length === 0) {
244
+ const wallets = walletInfos.map((info) => ({
245
+ address: info.address,
246
+ ethBalance: (0, viem_1.formatEther)(info.ethBalance),
247
+ agentBalance: (0, viem_1.formatEther)(info.agentBalance),
248
+ miners: [],
249
+ }));
250
+ return JSON.stringify(wallets);
251
+ }
252
+ // Phase 3: miner stats + art
253
+ const uncachedTokenIds = tokenIds.filter((id) => !artCache.has(id.toString()));
254
+ const FIELDS_PER_TOKEN = 5;
255
+ const detailContracts = tokenIds.flatMap((tokenId) => [
256
+ { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "rarity", args: [tokenId] },
257
+ { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "hashpower", args: [tokenId] },
258
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "tokenMineCount", args: [tokenId] },
259
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "tokenEarnings", args: [tokenId] },
260
+ { address: miningAgentAddress, abi: MiningAgentAbi, functionName: "mintBlock", args: [tokenId] },
261
+ ]);
262
+ const detailResults = await publicClient.multicall({ contracts: detailContracts });
263
+ // Batch art URI calls for uncached tokens — use small chunks to avoid RPC response size limits
264
+ const ART_CHUNK_SIZE = 2;
265
+ const artResults = [];
266
+ for (let i = 0; i < uncachedTokenIds.length; i += ART_CHUNK_SIZE) {
267
+ const chunk = uncachedTokenIds.slice(i, i + ART_CHUNK_SIZE);
268
+ const artContracts = chunk.map((tokenId) => ({
269
+ address: miningAgentAddress,
270
+ abi: MiningAgentAbi,
271
+ functionName: "tokenURI",
272
+ args: [tokenId],
273
+ }));
274
+ try {
275
+ const chunkResults = await publicClient.multicall({ contracts: artContracts });
276
+ artResults.push(...chunkResults);
277
+ }
278
+ catch {
279
+ // Push failure entries so indexing stays aligned
280
+ for (let j = 0; j < chunk.length; j++) {
281
+ artResults.push({ status: "failure" });
282
+ }
283
+ }
284
+ }
285
+ // Populate art cache
286
+ for (let i = 0; i < uncachedTokenIds.length; i++) {
287
+ const r = artResults[i];
288
+ if (r && r.status === "success" && r.result) {
289
+ const imageUri = parseArtFromTokenUri(r.result);
290
+ if (imageUri)
291
+ artCache.set(uncachedTokenIds[i].toString(), imageUri);
292
+ }
293
+ }
294
+ // Build wallet data
295
+ const minersByWallet = new Map();
296
+ for (let ti = 0; ti < tokenIds.length; ti++) {
297
+ const originalIdx = validTokenMap[ti];
298
+ const { walletIdx } = tokenIdMap[originalIdx];
299
+ const b = ti * FIELDS_PER_TOKEN;
300
+ const rarity = Number(detailResults[b].result ?? 0n);
301
+ const hashpower = Number(detailResults[b + 1].result ?? 100n);
302
+ const mineCount = detailResults[b + 2].result ?? 0n;
303
+ const earnings = detailResults[b + 3].result ?? 0n;
304
+ const mintBlock = detailResults[b + 4].result ?? 0n;
305
+ const tid = tokenIds[ti].toString();
306
+ const miner = {
307
+ tokenId: tid,
308
+ rarity,
309
+ rarityLabel: RARITY_LABELS[rarity] ?? `Tier ${rarity}`,
310
+ hashpower,
311
+ mineCount: mineCount.toString(),
312
+ earnings: (0, viem_1.formatEther)(earnings),
313
+ mintBlock: mintBlock.toString(),
314
+ imageUri: artCache.get(tid),
315
+ };
316
+ if (!minersByWallet.has(walletIdx))
317
+ minersByWallet.set(walletIdx, []);
318
+ minersByWallet.get(walletIdx).push(miner);
319
+ }
320
+ const wallets = walletInfos.map((info, i) => ({
321
+ address: info.address,
322
+ ethBalance: (0, viem_1.formatEther)(info.ethBalance),
323
+ agentBalance: (0, viem_1.formatEther)(info.agentBalance),
324
+ miners: minersByWallet.get(i) ?? [],
325
+ }));
326
+ return JSON.stringify(wallets);
327
+ }
328
+ async function handleNetwork() {
329
+ const results = await publicClient.multicall({
330
+ contracts: [
331
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "totalMines" },
332
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "totalMinted" },
333
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "miningTarget" },
334
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "MINEABLE_SUPPLY" },
335
+ { address: agentCoinAddress, abi: AgentCoinAbi, functionName: "ERA_INTERVAL" },
336
+ ],
337
+ });
338
+ const totalMines = results[0].result;
339
+ const totalMinted = results[1].result;
340
+ const miningTarget = results[2].result;
341
+ const mineableSupply = results[3].result;
342
+ const eraInterval = results[4].result;
343
+ const era = totalMines / eraInterval;
344
+ const minesUntilNextEra = eraInterval - (totalMines % eraInterval);
345
+ const supplyPct = Number((totalMinted * 10000n) / mineableSupply) / 100;
346
+ let baseReward = 3;
347
+ for (let i = 0n; i < era; i++) {
348
+ baseReward *= 0.9;
349
+ }
350
+ const nextEraReward = baseReward * 0.9;
351
+ const targetLog = Math.log2(Number(miningTarget));
352
+ const difficulty = targetLog > 250 ? "very easy" :
353
+ targetLog > 240 ? "easy" :
354
+ targetLog > 220 ? "moderate" :
355
+ targetLog > 200 ? "hard" : "very hard";
356
+ return JSON.stringify({
357
+ totalMines: totalMines.toString(),
358
+ totalMinted: (0, viem_1.formatEther)(totalMinted),
359
+ mineableSupply: (0, viem_1.formatEther)(mineableSupply),
360
+ era: Number(era),
361
+ minesUntilNextEra: minesUntilNextEra.toString(),
362
+ baseReward,
363
+ nextEraReward,
364
+ difficulty,
365
+ supplyPct,
366
+ });
367
+ }
368
+ function handleFleets() {
369
+ const fleets = getFleets(walletsPath);
370
+ return JSON.stringify(fleets.map((f) => ({ name: f.name, walletCount: f.addresses.length })));
371
+ }
372
+ function handleConfig() {
373
+ const rpcIsDefault = rpcUrl === DEFAULT_RPC;
374
+ const walletCount = getWalletAddresses(walletsPath).length;
375
+ return JSON.stringify({ rpcIsDefault, walletCount });
376
+ }
377
+ function jsonResponse(res, body, status = 200) {
378
+ res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
379
+ res.end(body);
380
+ }
381
+ function errorResponse(res, message, status = 500) {
382
+ jsonResponse(res, JSON.stringify({ error: message }), status);
383
+ }
384
+ const server = http.createServer(async (req, res) => {
385
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
386
+ const pathname = url.pathname;
387
+ try {
388
+ if (pathname === "/" || pathname === "") {
389
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
390
+ res.end(htmlPage);
391
+ return;
392
+ }
393
+ if (pathname === "/api/wallets") {
394
+ const fleet = url.searchParams.get("fleet");
395
+ const body = await handleWallets(fleet);
396
+ jsonResponse(res, body);
397
+ return;
398
+ }
399
+ if (pathname === "/api/network") {
400
+ const body = await handleNetwork();
401
+ jsonResponse(res, body);
402
+ return;
403
+ }
404
+ if (pathname === "/api/fleets") {
405
+ const body = handleFleets();
406
+ jsonResponse(res, body);
407
+ return;
408
+ }
409
+ if (pathname === "/api/config") {
410
+ const body = handleConfig();
411
+ jsonResponse(res, body);
412
+ return;
413
+ }
414
+ res.writeHead(404, { "Content-Type": "text/plain" });
415
+ res.end("Not Found");
416
+ }
417
+ catch (err) {
418
+ const message = err instanceof Error ? err.message : "Unknown error";
419
+ errorResponse(res, message);
420
+ }
421
+ });
422
+ server.listen(port);
423
+ return server;
424
+ }
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  const node_fs_1 = require("node:fs");
41
41
  const node_path_1 = require("node:path");
42
+ const node_child_process_1 = require("node:child_process");
42
43
  const commander_1 = require("commander");
43
44
  const viem_1 = require("viem");
44
45
  const MiningAgent_json_1 = __importDefault(require("./abi/MiningAgent.json"));
@@ -470,8 +471,198 @@ async function main() {
470
471
  console.log(` Tx: ${ui.dim((0, explorer_1.txUrl)(receipt.transactionHash))}`);
471
472
  console.log("");
472
473
  });
474
+ // --- Dashboard commands ---
475
+ const dashboardCmd = program
476
+ .command("dashboard")
477
+ .description("Multi-wallet mining dashboard");
478
+ dashboardCmd
479
+ .command("start", { isDefault: true })
480
+ .description("Launch the dashboard web UI")
481
+ .action(async () => {
482
+ const walletsPath = getWalletsPath();
483
+ // Seed wallets.json if it doesn't exist
484
+ if (!(0, node_fs_1.existsSync)(walletsPath)) {
485
+ const walletsDir = (0, node_path_1.join)(process.env.HOME ?? "", ".apow");
486
+ if (!(0, node_fs_1.existsSync)(walletsDir))
487
+ (0, node_fs_1.mkdirSync)(walletsDir, { recursive: true });
488
+ const initial = wallet_1.account ? [wallet_1.account.address] : [];
489
+ (0, node_fs_1.writeFileSync)(walletsPath, JSON.stringify(initial, null, 2), "utf8");
490
+ if (wallet_1.account) {
491
+ ui.ok(`Seeded ${walletsPath} with ${wallet_1.account.address.slice(0, 6)}...${wallet_1.account.address.slice(-4)}`);
492
+ }
493
+ else {
494
+ ui.ok(`Created ${walletsPath} (empty — add wallets with: apow dashboard add <address>)`);
495
+ }
496
+ }
497
+ // Auto-detect wallets from CWD
498
+ const { addresses, newCount } = detectWallets(process.cwd());
499
+ if (newCount > 0) {
500
+ ui.ok(`Detected ${addresses.length} wallets (${newCount} new)`);
501
+ }
502
+ else if (addresses.length > 0) {
503
+ console.log(` ${ui.dim(`${addresses.length} wallets loaded`)}`);
504
+ }
505
+ const { startDashboardServer } = await Promise.resolve().then(() => __importStar(require("./dashboard")));
506
+ console.log("");
507
+ console.log(` ${ui.bold("APoW Dashboard")} starting on http://localhost:3847`);
508
+ console.log(` ${ui.dim("Press Ctrl+C to stop")}`);
509
+ console.log("");
510
+ const server = startDashboardServer({
511
+ port: 3847,
512
+ walletsPath,
513
+ rpcUrl: config_1.config.rpcUrl,
514
+ miningAgentAddress: config_1.config.miningAgentAddress,
515
+ agentCoinAddress: config_1.config.agentCoinAddress,
516
+ });
517
+ // Open browser after short delay (server starts instantly)
518
+ setTimeout(() => {
519
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
520
+ (0, node_child_process_1.spawn)(openCmd, ["http://localhost:3847"], { stdio: "ignore" });
521
+ }, 500);
522
+ // Wait for SIGINT
523
+ await new Promise((resolve) => {
524
+ process.on("SIGINT", () => {
525
+ server.close();
526
+ resolve();
527
+ });
528
+ });
529
+ });
530
+ dashboardCmd
531
+ .command("add <address>")
532
+ .description("Add a wallet address to monitor")
533
+ .action((address) => {
534
+ if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
535
+ ui.error("Invalid address. Must be 0x + 40 hex characters.");
536
+ return;
537
+ }
538
+ const walletsPath = getWalletsPath();
539
+ const wallets = loadWallets(walletsPath);
540
+ const lower = address.toLowerCase();
541
+ if (wallets.some((w) => w.toLowerCase() === lower)) {
542
+ ui.warn("Address already monitored.");
543
+ return;
544
+ }
545
+ wallets.push(address);
546
+ saveWallets(walletsPath, wallets);
547
+ ui.ok(`Added ${address.slice(0, 6)}...${address.slice(-4)} (${wallets.length} wallets total)`);
548
+ });
549
+ dashboardCmd
550
+ .command("remove <address>")
551
+ .description("Remove a wallet address from monitoring")
552
+ .action((address) => {
553
+ const walletsPath = getWalletsPath();
554
+ const wallets = loadWallets(walletsPath);
555
+ const lower = address.toLowerCase();
556
+ const filtered = wallets.filter((w) => w.toLowerCase() !== lower);
557
+ if (filtered.length === wallets.length) {
558
+ ui.warn("Address not found in wallet list.");
559
+ return;
560
+ }
561
+ saveWallets(walletsPath, filtered);
562
+ ui.ok(`Removed ${address.slice(0, 6)}...${address.slice(-4)} (${filtered.length} wallets remaining)`);
563
+ });
564
+ dashboardCmd
565
+ .command("scan [dir]")
566
+ .description("Auto-detect wallets from wallet-0x*.txt files in a directory")
567
+ .action((dir) => {
568
+ const scanDir = dir ?? process.cwd();
569
+ const { addresses, newCount } = detectWallets(scanDir);
570
+ console.log("");
571
+ if (addresses.length === 0) {
572
+ console.log(` No wallets found in ${scanDir}`);
573
+ console.log(` ${ui.dim("Expected files named wallet-0x<address>.txt")}`);
574
+ }
575
+ else {
576
+ console.log(` ${ui.bold("Detected Wallets")} (${newCount} new, ${addresses.length} total)`);
577
+ console.log("");
578
+ for (const addr of addresses) {
579
+ console.log(` ${addr}`);
580
+ }
581
+ }
582
+ console.log("");
583
+ });
584
+ dashboardCmd
585
+ .command("wallets")
586
+ .description("List monitored wallet addresses")
587
+ .action(() => {
588
+ const walletsPath = getWalletsPath();
589
+ const wallets = loadWallets(walletsPath);
590
+ if (wallets.length === 0) {
591
+ console.log(" No wallets configured. Run: apow dashboard add <address>");
592
+ return;
593
+ }
594
+ console.log("");
595
+ console.log(` ${ui.bold("Monitored Wallets")} (${wallets.length})`);
596
+ console.log("");
597
+ for (const w of wallets) {
598
+ console.log(` ${w}`);
599
+ }
600
+ console.log("");
601
+ });
473
602
  await program.parseAsync(process.argv);
474
603
  }
604
+ function getWalletsPath() {
605
+ return (0, node_path_1.join)(process.env.HOME ?? "", ".apow", "wallets.json");
606
+ }
607
+ function loadWallets(path) {
608
+ try {
609
+ const raw = (0, node_fs_1.readFileSync)(path, "utf8");
610
+ const data = JSON.parse(raw);
611
+ return Array.isArray(data) ? data.filter((a) => typeof a === "string") : [];
612
+ }
613
+ catch {
614
+ return [];
615
+ }
616
+ }
617
+ function saveWallets(path, wallets) {
618
+ const dir = (0, node_path_1.join)(process.env.HOME ?? "", ".apow");
619
+ if (!(0, node_fs_1.existsSync)(dir))
620
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
621
+ (0, node_fs_1.writeFileSync)(path, JSON.stringify(wallets, null, 2), "utf8");
622
+ }
623
+ function detectWallets(scanDir) {
624
+ const walletsPath = getWalletsPath();
625
+ const existing = loadWallets(walletsPath);
626
+ const seen = new Set(existing.map((a) => a.toLowerCase()));
627
+ const detected = [];
628
+ // Scan scanDir for wallet-0x*.txt files
629
+ try {
630
+ const entries = (0, node_fs_1.readdirSync)(scanDir, { withFileTypes: true });
631
+ for (const entry of entries) {
632
+ if (entry.isFile()) {
633
+ const match = entry.name.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
634
+ if (match && !seen.has(match[1].toLowerCase())) {
635
+ detected.push(match[1]);
636
+ seen.add(match[1].toLowerCase());
637
+ }
638
+ }
639
+ // Scan rig*/wallet-0x*.txt subdirectories
640
+ if (entry.isDirectory() && entry.name.startsWith("rig")) {
641
+ try {
642
+ const rigFiles = (0, node_fs_1.readdirSync)((0, node_path_1.join)(scanDir, entry.name));
643
+ for (const file of rigFiles) {
644
+ const m = file.match(/^wallet-(0x[0-9a-fA-F]{40})\.txt$/);
645
+ if (m && !seen.has(m[1].toLowerCase())) {
646
+ detected.push(m[1]);
647
+ seen.add(m[1].toLowerCase());
648
+ }
649
+ }
650
+ }
651
+ catch {
652
+ // rig dir not readable — skip
653
+ }
654
+ }
655
+ }
656
+ }
657
+ catch {
658
+ // scanDir not readable
659
+ }
660
+ const merged = [...existing, ...detected];
661
+ if (detected.length > 0) {
662
+ saveWallets(walletsPath, merged);
663
+ }
664
+ return { addresses: merged, newCount: detected.length };
665
+ }
475
666
  main().catch((error) => {
476
667
  const message = error instanceof Error ? error.message : String(error);
477
668
  ui.error(message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apow-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Mine AGENT tokens on Base L2 with AI-powered proof of work",
5
5
  "keywords": [
6
6
  "apow",
package/skill.md CHANGED
@@ -23,6 +23,15 @@ metadata:
23
23
  configure the miner, mint, and mine. The only step that requires your user is funding the wallet
24
24
  with ETH on Base. Everything else you handle yourself.
25
25
 
26
+ > **IMPORTANT — You need a dedicated RPC endpoint.** The default public RPC (`https://mainnet.base.org`) has aggressive rate limits and **will cause minting and mining to fail**. Before starting, get a free Alchemy endpoint:
27
+ >
28
+ > 1. Go to [alchemy.com](https://www.alchemy.com/) and sign up (free, no credit card)
29
+ > 2. Click **Create new app** → Name: `apow-miner` → Chain: **Base** → Network: **Base Mainnet**
30
+ > 3. Copy the **HTTPS** URL: `https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY`
31
+ > 4. Set `RPC_URL` to this URL in your `.env`
32
+ >
33
+ > Alchemy's free tier (300M compute units/month) is more than enough. See [RPC Recommendations](#rpc-recommendations) for alternatives.
34
+
26
35
  ---
27
36
 
28
37
  ## Quick Start (TL;DR)
@@ -56,6 +65,7 @@ npx apow-cli wallet new
56
65
  # 2. Write .env directly (no interactive prompts needed)
57
66
  # IMPORTANT: Use an API-based provider (openai/anthropic/gemini), NOT claude-code/codex.
58
67
  # Session-based providers are too slow for the 20-second mint challenge window.
68
+ # IMPORTANT: The public Base RPC is unreliable — get a free Alchemy URL (see RPC Recommendations).
59
69
  cat > .env << 'EOF'
60
70
  PRIVATE_KEY=0x<from step 1>
61
71
  RPC_URL=https://mainnet.base.org
@@ -243,7 +253,8 @@ LLM_MODEL=gpt-4o-mini
243
253
 
244
254
  # === Network ===
245
255
 
246
- # Base RPC endpoint (default: https://mainnet.base.org)
256
+ # Base RPC endpoint — the public default is unreliable for sustained mining.
257
+ # Strongly recommend a free Alchemy key: https://www.alchemy.com/ (no credit card)
247
258
  RPC_URL=https://mainnet.base.org
248
259
 
249
260
  # Chain: "base" | "baseSepolia" (auto-detected from RPC_URL if omitted)
@@ -260,7 +271,7 @@ CHAIN=base
260
271
  | `LLM_PROVIDER` | No | `openai` | LLM provider: `openai`, `anthropic`, `ollama`, `gemini`, `claude-code`, or `codex` |
261
272
  | `LLM_API_KEY` | Conditional | -- | API key. Falls back to `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` per provider. Not needed for `ollama`, `claude-code`, or `codex` |
262
273
  | `LLM_MODEL` | No | `gpt-4o-mini` | Model identifier passed to the provider |
263
- | `RPC_URL` | No | `https://mainnet.base.org` | Base JSON-RPC endpoint |
274
+ | `RPC_URL` | **Strongly recommended** | `https://mainnet.base.org` | Base JSON-RPC endpoint. **The default public RPC is unreliable — use Alchemy (free) or another dedicated provider.** |
264
275
  | `CHAIN` | No | `base` | Network selector; auto-detects `baseSepolia` if RPC URL contains "sepolia" |
265
276
  | `SOLANA_RPC_URL` | No | `https://api.mainnet-beta.solana.com` | Solana RPC endpoint (only for `apow fund --solana`) |
266
277
  | `SQUID_INTEGRATOR_ID` | No | -- | Squid Router integrator ID for deposit address flow (free at [squidrouter.com](https://app.squidrouter.com/)) |