decharge-scout 4.0.0 → 4.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Vercel API Endpoint - Alpha Contributions
3
+ *
4
+ * GET /api/alpha - Get verified alpha contributions
5
+ * POST /api/alpha - Submit new alpha contribution
6
+ */
7
+
8
+ import { createClient } from '@supabase/supabase-js';
9
+
10
+ export const config = {
11
+ runtime: 'edge',
12
+ };
13
+
14
+ export default async function handler(req) {
15
+ // CORS headers
16
+ const headers = {
17
+ 'Access-Control-Allow-Origin': '*',
18
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
19
+ 'Access-Control-Allow-Headers': 'Content-Type',
20
+ 'Content-Type': 'application/json',
21
+ };
22
+
23
+ // Handle OPTIONS request for CORS
24
+ if (req.method === 'OPTIONS') {
25
+ return new Response(null, { status: 200, headers });
26
+ }
27
+
28
+ // Initialize Supabase client
29
+ const supabase = createClient(
30
+ process.env.SUPABASE_URL,
31
+ process.env.SUPABASE_ANON_KEY
32
+ );
33
+
34
+ // GET - Retrieve alpha contributions
35
+ if (req.method === 'GET') {
36
+ try {
37
+ const url = new URL(req.url);
38
+ const location = url.searchParams.get('location');
39
+ const verified = url.searchParams.get('verified') === 'true';
40
+ const minConfidence = parseFloat(url.searchParams.get('min_confidence') || '0');
41
+
42
+ let query = supabase
43
+ .from('alpha_contributions')
44
+ .select('*')
45
+ .order('created_at', { ascending: false })
46
+ .limit(100);
47
+
48
+ // Apply filters
49
+ if (location) {
50
+ query = query.ilike('location', `%${location}%`);
51
+ }
52
+
53
+ if (verified) {
54
+ query = query.eq('verified', true);
55
+ }
56
+
57
+ if (minConfidence > 0) {
58
+ query = query.gte('confidence', minConfidence);
59
+ }
60
+
61
+ const { data, error } = await query;
62
+
63
+ if (error) throw error;
64
+
65
+ // Get statistics
66
+ const { data: stats } = await supabase
67
+ .from('alpha_by_location')
68
+ .select('*')
69
+ .order('verified_count', { ascending: false })
70
+ .limit(10);
71
+
72
+ return new Response(
73
+ JSON.stringify({
74
+ contributions: data || [],
75
+ statistics: stats || [],
76
+ total: data?.length || 0,
77
+ timestamp: new Date().toISOString(),
78
+ }),
79
+ { status: 200, headers }
80
+ );
81
+ } catch (error) {
82
+ console.error('Alpha GET error:', error);
83
+ return new Response(
84
+ JSON.stringify({
85
+ error: 'Failed to fetch alpha contributions',
86
+ message: error.message,
87
+ }),
88
+ { status: 500, headers }
89
+ );
90
+ }
91
+ }
92
+
93
+ // POST - Submit alpha contribution
94
+ if (req.method === 'POST') {
95
+ try {
96
+ const data = await req.json();
97
+
98
+ // Validate required fields
99
+ if (!data.agent_name || !data.location || !data.contribution) {
100
+ return new Response(
101
+ JSON.stringify({ error: 'Missing required fields' }),
102
+ { status: 400, headers }
103
+ );
104
+ }
105
+
106
+ const { contribution } = data;
107
+
108
+ // Validate contribution structure
109
+ if (
110
+ typeof contribution.startHour !== 'number' ||
111
+ typeof contribution.endHour !== 'number' ||
112
+ !contribution.type ||
113
+ contribution.startHour < 0 ||
114
+ contribution.startHour > 23 ||
115
+ contribution.endHour < 0 ||
116
+ contribution.endHour > 23
117
+ ) {
118
+ return new Response(
119
+ JSON.stringify({ error: 'Invalid contribution data' }),
120
+ { status: 400, headers }
121
+ );
122
+ }
123
+
124
+ // Insert alpha contribution
125
+ const { data: insertedData, error: insertError } = await supabase
126
+ .from('alpha_contributions')
127
+ .insert({
128
+ agent_name: data.agent_name,
129
+ location: data.location,
130
+ contribution_type: contribution.type,
131
+ start_hour: contribution.startHour,
132
+ end_hour: contribution.endHour,
133
+ verified: data.verified || false,
134
+ confidence: data.confidence || 0.5,
135
+ verification_reasons: data.verificationReasons || [],
136
+ timestamp: new Date(data.timestamp || Date.now()).toISOString(),
137
+ })
138
+ .select()
139
+ .single();
140
+
141
+ if (insertError) throw insertError;
142
+
143
+ return new Response(
144
+ JSON.stringify({
145
+ success: true,
146
+ message: 'Alpha contribution submitted successfully',
147
+ contribution: insertedData,
148
+ }),
149
+ { status: 200, headers }
150
+ );
151
+ } catch (error) {
152
+ console.error('Alpha POST error:', error);
153
+ return new Response(
154
+ JSON.stringify({
155
+ error: 'Failed to submit alpha contribution',
156
+ message: error.message,
157
+ }),
158
+ { status: 500, headers }
159
+ );
160
+ }
161
+ }
162
+
163
+ return new Response(
164
+ JSON.stringify({ error: 'Method not allowed' }),
165
+ { status: 405, headers }
166
+ );
167
+ }
@@ -65,6 +65,29 @@ export default async function handler(req) {
65
65
 
66
66
  if (submissionError) throw submissionError;
67
67
 
68
+ // Handle alpha contribution if present
69
+ if (data.alpha_contribution) {
70
+ const alpha = data.alpha_contribution;
71
+ const { error: alphaError } = await supabase
72
+ .from('alpha_contributions')
73
+ .insert({
74
+ agent_name: data.agent_name,
75
+ location: data.location,
76
+ contribution_type: alpha.type,
77
+ start_hour: alpha.startHour,
78
+ end_hour: alpha.endHour,
79
+ verified: alpha.verified || false,
80
+ confidence: alpha.confidence || 0.5,
81
+ verification_reasons: alpha.verificationReasons || [],
82
+ timestamp: new Date(data.timestamp).toISOString(),
83
+ });
84
+
85
+ // Don't fail the entire request if alpha submission fails
86
+ if (alphaError) {
87
+ console.warn('Alpha contribution failed:', alphaError);
88
+ }
89
+ }
90
+
68
91
  // Update agent heartbeat (upsert)
69
92
  const { error: heartbeatError } = await supabase
70
93
  .from('agent_heartbeat')
@@ -0,0 +1,185 @@
1
+ -- DeCharge Scout Dashboard - Alpha Contributions Migration
2
+ -- Add this to your Supabase database to support verified local alpha contributions
3
+
4
+ -- ============================================================================
5
+ -- TABLE: alpha_contributions
6
+ -- Stores verified local knowledge contributions from agents
7
+ -- ============================================================================
8
+ CREATE TABLE IF NOT EXISTS alpha_contributions (
9
+ id SERIAL PRIMARY KEY,
10
+ agent_name VARCHAR(255) NOT NULL,
11
+ location VARCHAR(255) NOT NULL,
12
+ contribution_type VARCHAR(20) NOT NULL CHECK (contribution_type IN ('peak', 'cheap', 'general')),
13
+ start_hour INTEGER NOT NULL CHECK (start_hour >= 0 AND start_hour <= 23),
14
+ end_hour INTEGER NOT NULL CHECK (end_hour >= 0 AND end_hour <= 23),
15
+
16
+ -- Verification fields
17
+ verified BOOLEAN DEFAULT FALSE,
18
+ confidence DECIMAL(4, 3) CHECK (confidence >= 0 AND confidence <= 1),
19
+ verification_reasons TEXT[],
20
+
21
+ -- Metadata
22
+ timestamp TIMESTAMP NOT NULL,
23
+ created_at TIMESTAMP DEFAULT NOW(),
24
+
25
+ -- Indexes for performance
26
+ CONSTRAINT valid_hour_range CHECK (start_hour <= end_hour)
27
+ );
28
+
29
+ -- Indexes for efficient querying
30
+ CREATE INDEX idx_alpha_location ON alpha_contributions(location);
31
+ CREATE INDEX idx_alpha_verified ON alpha_contributions(verified);
32
+ CREATE INDEX idx_alpha_confidence ON alpha_contributions(confidence DESC);
33
+ CREATE INDEX idx_alpha_created_at ON alpha_contributions(created_at DESC);
34
+ CREATE INDEX idx_alpha_type ON alpha_contributions(contribution_type);
35
+
36
+ -- ============================================================================
37
+ -- VIEW: Verified Alpha Contributions (high confidence only)
38
+ -- ============================================================================
39
+ CREATE OR REPLACE VIEW verified_alpha_contributions AS
40
+ SELECT
41
+ id,
42
+ agent_name,
43
+ location,
44
+ contribution_type,
45
+ start_hour,
46
+ end_hour,
47
+ confidence,
48
+ verification_reasons,
49
+ timestamp,
50
+ created_at
51
+ FROM alpha_contributions
52
+ WHERE verified = TRUE AND confidence >= 0.6
53
+ ORDER BY confidence DESC, created_at DESC;
54
+
55
+ -- ============================================================================
56
+ -- VIEW: Alpha Contributions Summary by Location
57
+ -- ============================================================================
58
+ CREATE OR REPLACE VIEW alpha_by_location AS
59
+ SELECT
60
+ location,
61
+ COUNT(*) as total_contributions,
62
+ COUNT(CASE WHEN verified = TRUE THEN 1 END) as verified_count,
63
+ AVG(confidence) as avg_confidence,
64
+ MAX(created_at) as latest_contribution
65
+ FROM alpha_contributions
66
+ WHERE created_at > NOW() - INTERVAL '30 days'
67
+ GROUP BY location
68
+ ORDER BY verified_count DESC, total_contributions DESC;
69
+
70
+ -- ============================================================================
71
+ -- VIEW: Peak Hours Analysis (most commonly reported)
72
+ -- ============================================================================
73
+ CREATE OR REPLACE VIEW peak_hours_analysis AS
74
+ WITH hour_counts AS (
75
+ SELECT
76
+ location,
77
+ generate_series(start_hour, end_hour) as hour,
78
+ confidence,
79
+ contribution_type
80
+ FROM alpha_contributions
81
+ WHERE contribution_type = 'peak'
82
+ AND verified = TRUE
83
+ AND created_at > NOW() - INTERVAL '30 days'
84
+ )
85
+ SELECT
86
+ location,
87
+ hour,
88
+ COUNT(*) as report_count,
89
+ AVG(confidence) as avg_confidence,
90
+ COUNT(*) * AVG(confidence) as weighted_score
91
+ FROM hour_counts
92
+ GROUP BY location, hour
93
+ HAVING COUNT(*) >= 2 -- At least 2 reports
94
+ ORDER BY location, weighted_score DESC;
95
+
96
+ -- ============================================================================
97
+ -- FUNCTION: Get Alpha Insights for Location
98
+ -- ============================================================================
99
+ CREATE OR REPLACE FUNCTION get_alpha_insights(
100
+ p_location VARCHAR(255),
101
+ p_min_confidence DECIMAL DEFAULT 0.6
102
+ )
103
+ RETURNS TABLE (
104
+ contribution_type VARCHAR(20),
105
+ start_hour INTEGER,
106
+ end_hour INTEGER,
107
+ confidence DECIMAL(4, 3),
108
+ report_count BIGINT,
109
+ latest_report TIMESTAMP
110
+ ) AS $$
111
+ BEGIN
112
+ RETURN QUERY
113
+ SELECT
114
+ ac.contribution_type,
115
+ ac.start_hour,
116
+ ac.end_hour,
117
+ AVG(ac.confidence)::DECIMAL(4, 3) as confidence,
118
+ COUNT(*) as report_count,
119
+ MAX(ac.created_at) as latest_report
120
+ FROM alpha_contributions ac
121
+ WHERE ac.location ILIKE '%' || p_location || '%'
122
+ AND ac.verified = TRUE
123
+ AND ac.confidence >= p_min_confidence
124
+ AND ac.created_at > NOW() - INTERVAL '30 days'
125
+ GROUP BY ac.contribution_type, ac.start_hour, ac.end_hour
126
+ ORDER BY report_count DESC, confidence DESC
127
+ LIMIT 10;
128
+ END;
129
+ $$ LANGUAGE plpgsql;
130
+
131
+ -- ============================================================================
132
+ -- FUNCTION: Calculate Contribution Quality Score
133
+ -- ============================================================================
134
+ CREATE OR REPLACE FUNCTION calculate_contribution_quality(
135
+ p_agent_name VARCHAR(255)
136
+ )
137
+ RETURNS TABLE (
138
+ agent_name VARCHAR(255),
139
+ total_contributions BIGINT,
140
+ verified_contributions BIGINT,
141
+ avg_confidence DECIMAL(4, 3),
142
+ quality_score DECIMAL(6, 2)
143
+ ) AS $$
144
+ BEGIN
145
+ RETURN QUERY
146
+ SELECT
147
+ p_agent_name::VARCHAR(255),
148
+ COUNT(*)::BIGINT as total_contributions,
149
+ COUNT(CASE WHEN verified = TRUE THEN 1 END)::BIGINT as verified_contributions,
150
+ AVG(confidence)::DECIMAL(4, 3) as avg_confidence,
151
+ (
152
+ (COUNT(CASE WHEN verified = TRUE THEN 1 END)::DECIMAL / NULLIF(COUNT(*), 0)) * 100 +
153
+ (AVG(confidence) * 100)
154
+ )::DECIMAL(6, 2) as quality_score
155
+ FROM alpha_contributions
156
+ WHERE agent_name = p_agent_name;
157
+ END;
158
+ $$ LANGUAGE plpgsql;
159
+
160
+ -- ============================================================================
161
+ -- COMMENTS for documentation
162
+ -- ============================================================================
163
+ COMMENT ON TABLE alpha_contributions IS 'Stores verified local energy pricing knowledge from agents';
164
+ COMMENT ON COLUMN alpha_contributions.verified IS 'TRUE if contribution matches actual pricing data';
165
+ COMMENT ON COLUMN alpha_contributions.confidence IS 'Confidence score 0-1, higher means better match with actual data';
166
+ COMMENT ON COLUMN alpha_contributions.verification_reasons IS 'Array of reasons explaining verification result';
167
+
168
+ -- ============================================================================
169
+ -- Sample queries for testing
170
+ -- ============================================================================
171
+
172
+ -- Get all verified contributions for a location
173
+ -- SELECT * FROM verified_alpha_contributions WHERE location ILIKE '%mumbai%';
174
+
175
+ -- Get alpha insights for a specific location
176
+ -- SELECT * FROM get_alpha_insights('Mumbai', 0.7);
177
+
178
+ -- Get contribution quality for an agent
179
+ -- SELECT * FROM calculate_contribution_quality('Agent-X8DOAO');
180
+
181
+ -- Get peak hours analysis
182
+ -- SELECT * FROM peak_hours_analysis WHERE location ILIKE '%india%';
183
+
184
+ -- Get alpha summary by location
185
+ -- SELECT * FROM alpha_by_location ORDER BY verified_count DESC LIMIT 10;
@@ -213,6 +213,132 @@
213
213
  font-weight: bold;
214
214
  color: #667eea;
215
215
  }
216
+
217
+ .alpha-grid {
218
+ display: grid;
219
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
220
+ gap: 20px;
221
+ margin-top: 20px;
222
+ }
223
+
224
+ .alpha-card {
225
+ background: #f8f9fa;
226
+ padding: 20px;
227
+ border-radius: 8px;
228
+ border-left: 4px solid #4caf50;
229
+ transition: transform 0.2s;
230
+ }
231
+
232
+ .alpha-card:hover {
233
+ transform: translateY(-2px);
234
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
235
+ }
236
+
237
+ .alpha-card.unverified {
238
+ border-left-color: #ff9800;
239
+ opacity: 0.8;
240
+ }
241
+
242
+ .alpha-header {
243
+ display: flex;
244
+ justify-content: space-between;
245
+ align-items: center;
246
+ margin-bottom: 12px;
247
+ }
248
+
249
+ .alpha-type {
250
+ display: inline-block;
251
+ padding: 4px 10px;
252
+ background: #4caf50;
253
+ color: white;
254
+ border-radius: 12px;
255
+ font-size: 0.75em;
256
+ font-weight: 600;
257
+ text-transform: uppercase;
258
+ }
259
+
260
+ .alpha-type.cheap {
261
+ background: #2196f3;
262
+ }
263
+
264
+ .confidence-badge {
265
+ display: inline-block;
266
+ padding: 4px 10px;
267
+ border-radius: 12px;
268
+ font-size: 0.75em;
269
+ font-weight: 600;
270
+ }
271
+
272
+ .confidence-high {
273
+ background: #c8e6c9;
274
+ color: #2e7d32;
275
+ }
276
+
277
+ .confidence-medium {
278
+ background: #fff9c4;
279
+ color: #f57f17;
280
+ }
281
+
282
+ .confidence-low {
283
+ background: #ffccbc;
284
+ color: #d84315;
285
+ }
286
+
287
+ .alpha-hours {
288
+ font-size: 1.5em;
289
+ font-weight: bold;
290
+ color: #333;
291
+ margin: 10px 0;
292
+ }
293
+
294
+ .alpha-location {
295
+ color: #666;
296
+ font-size: 0.9em;
297
+ margin-bottom: 8px;
298
+ }
299
+
300
+ .alpha-agent {
301
+ color: #999;
302
+ font-size: 0.8em;
303
+ }
304
+
305
+ .verification-reason {
306
+ background: white;
307
+ padding: 8px;
308
+ border-radius: 4px;
309
+ margin-top: 10px;
310
+ font-size: 0.85em;
311
+ color: #666;
312
+ border-left: 3px solid #4caf50;
313
+ }
314
+
315
+ .verification-reason.warning {
316
+ border-left-color: #ff9800;
317
+ }
318
+
319
+ .verified-badge {
320
+ display: inline-flex;
321
+ align-items: center;
322
+ gap: 4px;
323
+ padding: 4px 8px;
324
+ background: #c8e6c9;
325
+ color: #2e7d32;
326
+ border-radius: 12px;
327
+ font-size: 0.75em;
328
+ font-weight: 600;
329
+ }
330
+
331
+ .unverified-badge {
332
+ display: inline-flex;
333
+ align-items: center;
334
+ gap: 4px;
335
+ padding: 4px 8px;
336
+ background: #ffccbc;
337
+ color: #d84315;
338
+ border-radius: 12px;
339
+ font-size: 0.75em;
340
+ font-weight: 600;
341
+ }
216
342
  </style>
217
343
  </head>
218
344
  <body>
@@ -255,6 +381,11 @@
255
381
  <div id="locations-map" class="map-grid"></div>
256
382
  </div>
257
383
 
384
+ <div class="section">
385
+ <h2>💡 Verified Local Alpha (Community Knowledge)</h2>
386
+ <div id="alpha-container"></div>
387
+ </div>
388
+
258
389
  <div class="section">
259
390
  <h2>📊 Recent Submissions</h2>
260
391
  <button class="refresh-btn" onclick="loadData()">Refresh Data</button>
@@ -295,6 +426,9 @@
295
426
  // Render submissions table
296
427
  renderSubmissionsTable(data.recentSubmissions || []);
297
428
 
429
+ // Load alpha contributions
430
+ await loadAlphaContributions();
431
+
298
432
  // Update timestamp
299
433
  document.getElementById('last-updated').textContent =
300
434
  `Last updated: ${new Date().toLocaleTimeString()}`;
@@ -308,6 +442,24 @@
308
442
  }
309
443
  }
310
444
 
445
+ async function loadAlphaContributions() {
446
+ try {
447
+ const response = await fetch(`${API_BASE}/api/alpha?verified=true&min_confidence=0.6`);
448
+
449
+ if (!response.ok) {
450
+ console.warn('Alpha API not available yet');
451
+ return;
452
+ }
453
+
454
+ const data = await response.json();
455
+ renderAlphaContributions(data.contributions || [], data.statistics || []);
456
+ } catch (error) {
457
+ console.warn('Error loading alpha contributions:', error);
458
+ document.getElementById('alpha-container').innerHTML =
459
+ '<div class="loading">Alpha contributions will appear here once the database is migrated</div>';
460
+ }
461
+ }
462
+
311
463
  function renderLocationsMap(locations) {
312
464
  const container = document.getElementById('locations-map');
313
465
 
@@ -325,6 +477,90 @@
325
477
  `).join('');
326
478
  }
327
479
 
480
+ function renderAlphaContributions(contributions, statistics) {
481
+ const container = document.getElementById('alpha-container');
482
+
483
+ if (contributions.length === 0) {
484
+ container.innerHTML = '<div class="loading">No verified alpha contributions yet. Share your local knowledge to be the first!</div>';
485
+ return;
486
+ }
487
+
488
+ // Show top locations first
489
+ let statsHtml = '';
490
+ if (statistics.length > 0) {
491
+ statsHtml = `
492
+ <div class="map-grid" style="margin-bottom: 20px;">
493
+ ${statistics.slice(0, 5).map(stat => `
494
+ <div class="location-card">
495
+ <h4>${escapeHtml(stat.location)}</h4>
496
+ <div class="count">${stat.verified_count}</div>
497
+ <div class="label">verified contributions</div>
498
+ <div style="margin-top: 8px; color: #666; font-size: 0.85em;">
499
+ Avg confidence: ${(stat.avg_confidence * 100).toFixed(0)}%
500
+ </div>
501
+ </div>
502
+ `).join('')}
503
+ </div>
504
+ `;
505
+ }
506
+
507
+ container.innerHTML = statsHtml + `
508
+ <div class="alpha-grid">
509
+ ${contributions.slice(0, 12).map(alpha => {
510
+ const confidence = parseFloat(alpha.confidence);
511
+ const confidenceClass = confidence >= 0.8 ? 'confidence-high' :
512
+ confidence >= 0.6 ? 'confidence-medium' : 'confidence-low';
513
+ const confidenceLabel = confidence >= 0.8 ? 'High' :
514
+ confidence >= 0.6 ? 'Medium' : 'Low';
515
+
516
+ const formatHour = (h) => {
517
+ const hour = parseInt(h);
518
+ if (hour === 0) return '12 AM';
519
+ if (hour < 12) return `${hour} AM`;
520
+ if (hour === 12) return '12 PM';
521
+ return `${hour - 12} PM`;
522
+ };
523
+
524
+ const typeClass = alpha.contribution_type === 'peak' ? 'alpha-type' : 'alpha-type cheap';
525
+
526
+ return `
527
+ <div class="alpha-card ${alpha.verified ? '' : 'unverified'}">
528
+ <div class="alpha-header">
529
+ <span class="${typeClass}">${alpha.contribution_type}</span>
530
+ ${alpha.verified
531
+ ? `<span class="verified-badge">✓ Verified</span>`
532
+ : `<span class="unverified-badge">⚠ Unverified</span>`
533
+ }
534
+ </div>
535
+
536
+ <div class="alpha-hours">
537
+ ${formatHour(alpha.start_hour)} - ${formatHour(alpha.end_hour)}
538
+ </div>
539
+
540
+ <div class="alpha-location">📍 ${escapeHtml(alpha.location)}</div>
541
+
542
+ <div class="alpha-agent">
543
+ By ${escapeHtml(alpha.agent_name)} • ${new Date(alpha.created_at).toLocaleDateString()}
544
+ </div>
545
+
546
+ <div style="margin-top: 12px;">
547
+ <span class="confidence-badge ${confidenceClass}">
548
+ ${confidenceLabel} Confidence (${(confidence * 100).toFixed(0)}%)
549
+ </span>
550
+ </div>
551
+
552
+ ${alpha.verification_reasons && alpha.verification_reasons.length > 0 ? `
553
+ <div class="verification-reason ${confidence < 0.6 ? 'warning' : ''}">
554
+ ${escapeHtml(alpha.verification_reasons[0])}
555
+ </div>
556
+ ` : ''}
557
+ </div>
558
+ `;
559
+ }).join('')}
560
+ </div>
561
+ `;
562
+ }
563
+
328
564
  function renderSubmissionsTable(submissions) {
329
565
  const container = document.getElementById('submissions-container');
330
566
 
package/index.js CHANGED
@@ -33,7 +33,7 @@ dotenv.config({ path: path.join(process.cwd(), '.env') });
33
33
  // Import modules
34
34
  import { startWalletServer, openWalletConnection, waitForWalletConnection, getConnectedWallet } from './src/wallet-server.js';
35
35
  import { setConnectedWallet, getBalance, checkBalance, mockStake, refundStake } from './src/browser-wallet.js';
36
- import { getWeatherForLocation } from './src/weather-data.js';
36
+ import { getWeatherForLocation, getFallbackCoordinates } from './src/weather-data.js';
37
37
  import { generateSmartPricing, getPricingInsights, getRegionalPricing } from './src/smart-pricing.js';
38
38
  import { findCheapestWindow, calculateSavings } from './src/optimizer.js';
39
39
  import { submitToOracle } from './src/oracle.js';
@@ -53,6 +53,7 @@ const CYCLE_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
53
53
  let isRunning = true;
54
54
  let totalRuns = 0;
55
55
  let stakeTransactionSignature = null;
56
+ let pendingAlphaContribution = null; // Store alpha contribution for next submission
56
57
 
57
58
  // Readline interface for prompts
58
59
  const rl = createInterface({
@@ -172,16 +173,26 @@ async function main(options) {
172
173
  const detectedLocation = await getLocation();
173
174
  locationSpinner.succeed(chalk.green(`📍 Detected Location: ${detectedLocation}`));
174
175
 
175
- // Ask user to confirm or override (no timeout - wait for input)
176
+ // Ask user to confirm or override with 10 second timeout
176
177
  console.log(chalk.blue('\nYou can use this location or enter a custom one.'));
177
178
  console.log(chalk.gray('(Press Enter to use detected location, or type a custom location like "Dallas,TX,USA")'));
179
+ console.log(chalk.gray('(Will auto-proceed in 10 seconds if no input)\n'));
178
180
 
179
181
  try {
180
- const customLocation = await question('Enter location (or press Enter for auto-detected): ');
182
+ // Create a promise race between user input and timeout
183
+ const customLocation = await Promise.race([
184
+ question('Enter location (or press Enter for auto-detected): '),
185
+ new Promise((resolve) => {
186
+ setTimeout(() => {
187
+ console.log(chalk.yellow('\n⏱️ Timeout - using detected location'));
188
+ resolve('');
189
+ }, 10000); // 10 second timeout
190
+ })
191
+ ]);
181
192
 
182
193
  // Use custom location if provided, otherwise use detected
183
- location = customLocation.trim() || detectedLocation;
184
- if (customLocation.trim()) {
194
+ location = (customLocation || '').trim() || detectedLocation;
195
+ if ((customLocation || '').trim()) {
185
196
  console.log(chalk.green(`✓ Using custom location: ${location}`));
186
197
  } else {
187
198
  console.log(chalk.green(`✓ Using detected location: ${location}`));
@@ -277,7 +288,38 @@ async function runQueryCycle(wallet, agentName, location, options) {
277
288
 
278
289
  } catch (error) {
279
290
  weatherSpinner.fail(chalk.red('Failed to fetch weather data'));
280
- throw new Error(`Unable to generate pricing simulation: ${error.message}`);
291
+
292
+ // Check if we have fallback coordinates available
293
+ const fallbackCoords = getFallbackCoordinates(location);
294
+
295
+ console.log(chalk.yellow(`\n⚠️ Could not geocode location: "${location}"`));
296
+ console.log(chalk.yellow(` Error: ${error.message}`));
297
+
298
+ if (fallbackCoords) {
299
+ console.log(chalk.blue(`\n💡 We have fallback coordinates for: ${fallbackCoords.name}, ${fallbackCoords.country}`));
300
+ }
301
+
302
+ console.log(chalk.blue(`\nOptions:`));
303
+ console.log(chalk.gray(` 1. Try a different location (e.g., just "Hyderabad" or "Hyderabad, India")`));
304
+ if (fallbackCoords) {
305
+ console.log(chalk.gray(` 2. Use fallback coordinates for ${fallbackCoords.name}`));
306
+ }
307
+ console.log(chalk.gray(` 3. Exit and restart\n`));
308
+
309
+ const userChoice = await question(chalk.blue('Enter new location (or press Enter to exit): '));
310
+
311
+ if (!userChoice.trim()) {
312
+ console.log(chalk.yellow('Exiting...'));
313
+ throw new Error('User chose to exit');
314
+ }
315
+
316
+ // Update location and retry this cycle
317
+ location = userChoice.trim();
318
+ console.log(chalk.green(`✓ Updated location to: ${location}`));
319
+ console.log(chalk.gray('Retrying...\n'));
320
+
321
+ // Retry with new location - this will loop back and try again
322
+ continue;
281
323
  }
282
324
 
283
325
  // Run optimization
@@ -341,14 +383,22 @@ async function runQueryCycle(wallet, agentName, location, options) {
341
383
  console.log(chalk.blue(`\n🌐 Dashboard API URL: ${apiUrl}`));
342
384
  console.log(chalk.gray(`📤 Submitting data...`));
343
385
 
386
+ const dashboardPayload = {
387
+ ...submissionData,
388
+ wallet: wallet, // wallet parameter from runQueryCycle
389
+ run_number: totalRuns
390
+ };
391
+
392
+ // Include alpha contribution if available
393
+ if (pendingAlphaContribution) {
394
+ dashboardPayload.alpha_contribution = pendingAlphaContribution;
395
+ console.log(chalk.blue(`📊 Including verified alpha contribution (${(pendingAlphaContribution.confidence * 100).toFixed(0)}% confidence)`));
396
+ }
397
+
344
398
  const response = await fetch(apiUrl, {
345
399
  method: 'POST',
346
400
  headers: { 'Content-Type': 'application/json' },
347
- body: JSON.stringify({
348
- ...submissionData,
349
- wallet: wallet, // wallet parameter from runQueryCycle
350
- run_number: totalRuns
351
- })
401
+ body: JSON.stringify(dashboardPayload)
352
402
  });
353
403
 
354
404
  const responseText = await response.text();
@@ -358,6 +408,12 @@ async function runQueryCycle(wallet, agentName, location, options) {
358
408
  dashboardSpinner.succeed(chalk.green('✅ Dashboard submission successful!'));
359
409
  console.log(chalk.green(`🎉 Data should now appear at: https://decharge-scout.vercel.app/agentone`));
360
410
  console.log(chalk.gray(`Response: ${responseText}`));
411
+
412
+ // Clear pending alpha contribution after successful submission
413
+ if (pendingAlphaContribution) {
414
+ console.log(chalk.green(` ✓ Alpha contribution synced to dashboard`));
415
+ pendingAlphaContribution = null;
416
+ }
361
417
  } else {
362
418
  dashboardSpinner.warn(chalk.yellow(`⚠️ Dashboard API returned: ${response.status}`));
363
419
  console.log(chalk.yellow(`Response: ${responseText}`));
@@ -420,6 +476,17 @@ async function runQueryCycle(wallet, agentName, location, options) {
420
476
  // Save contribution with verification status
421
477
  const contribution = saveAlphaContribution(parsed, agentName, location, verification);
422
478
 
479
+ // Store for dashboard submission
480
+ pendingAlphaContribution = {
481
+ type: parsed.type,
482
+ startHour: parsed.startHour,
483
+ endHour: parsed.endHour,
484
+ location: parsed.location,
485
+ verified: verification.verified,
486
+ confidence: verification.confidence,
487
+ verificationReasons: verification.reasons
488
+ };
489
+
423
490
  // Calculate bonus (higher for verified contributions)
424
491
  const alphaBonus = calculateAlphaBonus(parsed, verification);
425
492
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decharge-scout",
3
- "version": "4.0.0",
3
+ "version": "4.0.4",
4
4
  "description": "Global Energy Scout - Weather-powered intelligent energy price forecasting with Solana integration",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -12,34 +12,57 @@ import fetch from 'node-fetch';
12
12
  /**
13
13
  * Get coordinates from location string
14
14
  * Simple geocoding using Open-Meteo's geocoding API (also free!)
15
+ * Tries multiple search strategies for better success rate
15
16
  */
16
17
  export async function getCoordinatesFromLocation(location) {
17
- try {
18
- const locationEncoded = encodeURIComponent(location);
19
- const url = `https://geocoding-api.open-meteo.com/v1/search?name=${locationEncoded}&count=1&language=en&format=json`;
20
-
21
- const response = await fetch(url);
22
- if (!response.ok) {
23
- throw new Error(`Geocoding failed: ${response.status}`);
18
+ // Try multiple search strategies
19
+ const searchTerms = [
20
+ location, // Original: "Hyderabad, TS, IN"
21
+ location.split(',')[0].trim(), // Just city: "Hyderabad"
22
+ location.replace(/,\s*[A-Z]{2}\s*,/, ','), // Remove state: "Hyderabad, IN"
23
+ ];
24
+
25
+ // Remove duplicates
26
+ const uniqueSearchTerms = [...new Set(searchTerms)];
27
+
28
+ let lastError = null;
29
+
30
+ for (const searchTerm of uniqueSearchTerms) {
31
+ try {
32
+ const locationEncoded = encodeURIComponent(searchTerm);
33
+ const url = `https://geocoding-api.open-meteo.com/v1/search?name=${locationEncoded}&count=5&language=en&format=json`;
34
+
35
+ const response = await fetch(url);
36
+ if (!response.ok) {
37
+ lastError = new Error(`Geocoding failed: ${response.status}`);
38
+ continue;
39
+ }
40
+
41
+ const data = await response.json();
42
+
43
+ if (!data.results || data.results.length === 0) {
44
+ lastError = new Error(`Location "${searchTerm}" not found`);
45
+ continue;
46
+ }
47
+
48
+ // Use first result
49
+ const result = data.results[0];
50
+ console.log(`✓ Geocoded "${searchTerm}" → ${result.name}, ${result.country} (${result.latitude}, ${result.longitude})`);
51
+
52
+ return {
53
+ latitude: result.latitude,
54
+ longitude: result.longitude,
55
+ name: result.name,
56
+ country: result.country,
57
+ timezone: result.timezone || 'auto'
58
+ };
59
+ } catch (error) {
60
+ lastError = error;
61
+ continue;
24
62
  }
25
-
26
- const data = await response.json();
27
-
28
- if (!data.results || data.results.length === 0) {
29
- throw new Error('Location not found');
30
- }
31
-
32
- const result = data.results[0];
33
- return {
34
- latitude: result.latitude,
35
- longitude: result.longitude,
36
- name: result.name,
37
- country: result.country,
38
- timezone: result.timezone || 'UTC'
39
- };
40
- } catch (error) {
41
- throw new Error(`Failed to geocode location: ${error.message}`);
42
63
  }
64
+
65
+ throw new Error(`Failed to geocode location after trying: ${uniqueSearchTerms.join(', ')}. Last error: ${lastError?.message}`);
43
66
  }
44
67
 
45
68
  /**
@@ -95,23 +118,47 @@ export async function fetchWeatherForecast(latitude, longitude, timezone = 'auto
95
118
  }
96
119
  }
97
120
 
121
+ /**
122
+ * Known coordinates for common cities (fallback)
123
+ */
124
+ const KNOWN_CITIES = {
125
+ 'hyderabad': { latitude: 17.385, longitude: 78.487, name: 'Hyderabad', country: 'India', timezone: 'Asia/Kolkata' },
126
+ 'mumbai': { latitude: 19.076, longitude: 72.877, name: 'Mumbai', country: 'India', timezone: 'Asia/Kolkata' },
127
+ 'delhi': { latitude: 28.704, longitude: 77.102, name: 'Delhi', country: 'India', timezone: 'Asia/Kolkata' },
128
+ 'bangalore': { latitude: 12.972, longitude: 77.594, name: 'Bangalore', country: 'India', timezone: 'Asia/Kolkata' },
129
+ 'chennai': { latitude: 13.083, longitude: 80.270, name: 'Chennai', country: 'India', timezone: 'Asia/Kolkata' },
130
+ 'lagos': { latitude: 6.524, longitude: 3.379, name: 'Lagos', country: 'Nigeria', timezone: 'Africa/Lagos' },
131
+ 'london': { latitude: 51.509, longitude: -0.118, name: 'London', country: 'United Kingdom', timezone: 'Europe/London' },
132
+ 'new york': { latitude: 40.713, longitude: -74.006, name: 'New York', country: 'United States', timezone: 'America/New_York' },
133
+ 'los angeles': { latitude: 34.052, longitude: -118.244, name: 'Los Angeles', country: 'United States', timezone: 'America/Los_Angeles' },
134
+ 'chicago': { latitude: 41.878, longitude: -87.630, name: 'Chicago', country: 'United States', timezone: 'America/Chicago' },
135
+ 'houston': { latitude: 29.760, longitude: -95.369, name: 'Houston', country: 'United States', timezone: 'America/Chicago' },
136
+ 'dallas': { latitude: 32.776, longitude: -96.797, name: 'Dallas', country: 'United States', timezone: 'America/Chicago' },
137
+ };
138
+
98
139
  /**
99
140
  * Get weather data for a location
141
+ * Throws error if geocoding fails - caller should handle user interaction
100
142
  */
101
143
  export async function getWeatherForLocation(location) {
102
- try {
103
- // Get coordinates
104
- const coords = await getCoordinatesFromLocation(location);
105
- console.log(`📍 Location: ${coords.name}, ${coords.country} (${coords.latitude}, ${coords.longitude})`);
144
+ // Try geocoding - throw error if fails
145
+ const coords = await getCoordinatesFromLocation(location);
146
+ console.log(`📍 Location: ${coords.name}, ${coords.country} (${coords.latitude}, ${coords.longitude})`);
106
147
 
107
- // Get weather forecast
108
- const forecast = await fetchWeatherForecast(coords.latitude, coords.longitude, coords.timezone);
148
+ // Get weather forecast
149
+ const forecast = await fetchWeatherForecast(coords.latitude, coords.longitude, coords.timezone);
109
150
 
110
- return {
111
- location: coords,
112
- forecast
113
- };
114
- } catch (error) {
115
- throw new Error(`Failed to get weather for location: ${error.message}`);
116
- }
151
+ return {
152
+ location: coords,
153
+ forecast
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Get fallback coordinates for known cities
159
+ * Returns null if city not in database
160
+ */
161
+ export function getFallbackCoordinates(location) {
162
+ const cityKey = location.split(',')[0].trim().toLowerCase();
163
+ return KNOWN_CITIES[cityKey] || null;
117
164
  }