decharge-scout 1.0.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 +18 -0
- package/README.md +323 -0
- package/index.js +374 -0
- package/package.json +48 -0
- package/setup.js +389 -0
- package/src/energy-data.js +198 -0
- package/src/geolocation.js +150 -0
- package/src/optimizer.js +165 -0
- package/src/oracle.js +199 -0
- package/src/points.js +159 -0
- package/src/wallet.js +132 -0
- package/src/x402.js +245 -0
package/setup.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DeCharge Scout - Interactive Setup Script
|
|
5
|
+
*
|
|
6
|
+
* Automates the complete setup process:
|
|
7
|
+
* - Install dependencies
|
|
8
|
+
* - Generate wallet
|
|
9
|
+
* - Request devnet airdrop
|
|
10
|
+
* - Configure .env with prompts
|
|
11
|
+
* - Global installation
|
|
12
|
+
*
|
|
13
|
+
* Usage: node setup.js
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn, exec } from 'child_process';
|
|
17
|
+
import { promisify } from 'util';
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
19
|
+
import { createInterface } from 'readline';
|
|
20
|
+
import { Keypair, Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
import { dirname } from 'path';
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = dirname(__filename);
|
|
27
|
+
|
|
28
|
+
const execAsync = promisify(exec);
|
|
29
|
+
|
|
30
|
+
// ANSI color codes (avoiding external dependencies)
|
|
31
|
+
const colors = {
|
|
32
|
+
reset: '\x1b[0m',
|
|
33
|
+
bright: '\x1b[1m',
|
|
34
|
+
red: '\x1b[31m',
|
|
35
|
+
green: '\x1b[32m',
|
|
36
|
+
yellow: '\x1b[33m',
|
|
37
|
+
blue: '\x1b[34m',
|
|
38
|
+
cyan: '\x1b[36m',
|
|
39
|
+
magenta: '\x1b[35m'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const log = {
|
|
43
|
+
info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
|
|
44
|
+
success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
|
|
45
|
+
error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
|
|
46
|
+
warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
|
|
47
|
+
title: (msg) => console.log(`\n${colors.cyan}${colors.bright}${msg}${colors.reset}\n`)
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Create readline interface for prompts
|
|
51
|
+
const rl = createInterface({
|
|
52
|
+
input: process.stdin,
|
|
53
|
+
output: process.stdout
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Main setup function
|
|
60
|
+
*/
|
|
61
|
+
async function main() {
|
|
62
|
+
console.clear();
|
|
63
|
+
log.title('🔋 DeCharge Scout - Interactive Setup');
|
|
64
|
+
console.log('This will set up everything you need to run DeCharge Scout.\n');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Step 1: Check Node.js version
|
|
68
|
+
await checkNodeVersion();
|
|
69
|
+
|
|
70
|
+
// Step 2: Install dependencies
|
|
71
|
+
await installDependencies();
|
|
72
|
+
|
|
73
|
+
// Step 3: Configure environment
|
|
74
|
+
await configureEnvironment();
|
|
75
|
+
|
|
76
|
+
// Step 4: Setup wallet
|
|
77
|
+
const walletPath = await setupWallet();
|
|
78
|
+
|
|
79
|
+
// Step 5: Fund wallet
|
|
80
|
+
await fundWallet(walletPath);
|
|
81
|
+
|
|
82
|
+
// Step 6: Global installation
|
|
83
|
+
await globalInstallation();
|
|
84
|
+
|
|
85
|
+
// Step 7: Final summary
|
|
86
|
+
showFinalSummary(walletPath);
|
|
87
|
+
|
|
88
|
+
rl.close();
|
|
89
|
+
process.exit(0);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
log.error(`Setup failed: ${error.message}`);
|
|
92
|
+
rl.close();
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check Node.js version
|
|
99
|
+
*/
|
|
100
|
+
async function checkNodeVersion() {
|
|
101
|
+
log.info('Checking Node.js version...');
|
|
102
|
+
|
|
103
|
+
const version = process.version;
|
|
104
|
+
const majorVersion = parseInt(version.slice(1).split('.')[0]);
|
|
105
|
+
|
|
106
|
+
if (majorVersion < 20) {
|
|
107
|
+
throw new Error(`Node.js v20 or higher required. Current: ${version}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log.success(`Node.js ${version} detected`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Install npm dependencies
|
|
115
|
+
*/
|
|
116
|
+
async function installDependencies() {
|
|
117
|
+
log.info('Installing dependencies...');
|
|
118
|
+
|
|
119
|
+
// Check if node_modules exists
|
|
120
|
+
if (existsSync(path.join(__dirname, 'node_modules'))) {
|
|
121
|
+
const answer = await question('Dependencies already installed. Reinstall? (y/N): ');
|
|
122
|
+
if (answer.toLowerCase() !== 'y') {
|
|
123
|
+
log.info('Skipping dependency installation');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const npm = spawn('npm', ['install'], {
|
|
130
|
+
cwd: __dirname,
|
|
131
|
+
stdio: 'inherit'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
npm.on('close', (code) => {
|
|
135
|
+
if (code !== 0) {
|
|
136
|
+
reject(new Error('npm install failed'));
|
|
137
|
+
} else {
|
|
138
|
+
log.success('Dependencies installed');
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Configure .env file
|
|
147
|
+
*/
|
|
148
|
+
async function configureEnvironment() {
|
|
149
|
+
log.info('Configuring environment variables...');
|
|
150
|
+
|
|
151
|
+
const envPath = path.join(__dirname, '.env');
|
|
152
|
+
let envContent = {};
|
|
153
|
+
|
|
154
|
+
// Load existing .env if it exists
|
|
155
|
+
if (existsSync(envPath)) {
|
|
156
|
+
const answer = await question('.env file exists. Reconfigure? (y/N): ');
|
|
157
|
+
if (answer.toLowerCase() !== 'y') {
|
|
158
|
+
log.info('Keeping existing .env configuration');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Parse existing .env
|
|
163
|
+
const existing = readFileSync(envPath, 'utf-8');
|
|
164
|
+
existing.split('\n').forEach(line => {
|
|
165
|
+
const [key, value] = line.split('=');
|
|
166
|
+
if (key && value) {
|
|
167
|
+
envContent[key.trim()] = value.trim();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.log('\n--- Environment Configuration ---\n');
|
|
173
|
+
|
|
174
|
+
// EIA API Key (required)
|
|
175
|
+
console.log('EIA API Key (required for energy data)');
|
|
176
|
+
console.log('Get yours at: https://www.eia.gov/opendata/register.php');
|
|
177
|
+
const eiaKey = await question(`EIA_API_KEY [${envContent.EIA_API_KEY || 'none'}]: `);
|
|
178
|
+
if (eiaKey) envContent.EIA_API_KEY = eiaKey;
|
|
179
|
+
|
|
180
|
+
if (!envContent.EIA_API_KEY || envContent.EIA_API_KEY === 'your_eia_api_key_here') {
|
|
181
|
+
log.warn('No EIA API key provided. You can add it later to .env');
|
|
182
|
+
envContent.EIA_API_KEY = 'your_eia_api_key_here';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Solana configuration
|
|
186
|
+
const network = await question('Solana network [devnet]: ') || 'devnet';
|
|
187
|
+
envContent.SOLANA_NETWORK = network;
|
|
188
|
+
|
|
189
|
+
const rpcUrl = await question('Solana RPC URL [https://api.devnet.solana.com]: ') || 'https://api.devnet.solana.com';
|
|
190
|
+
envContent.SOLANA_RPC_URL = rpcUrl;
|
|
191
|
+
|
|
192
|
+
// Optional: Dashboard API
|
|
193
|
+
const dashboardUrl = await question('Dashboard API URL (optional, press Enter to skip): ');
|
|
194
|
+
if (dashboardUrl) {
|
|
195
|
+
envContent.DASHBOARD_API_URL = dashboardUrl;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Escrow address (use default)
|
|
199
|
+
envContent.ORACLE_ESCROW_ADDRESS = envContent.ORACLE_ESCROW_ADDRESS || '4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T';
|
|
200
|
+
envContent.STAKE_AMOUNT = envContent.STAKE_AMOUNT || '0.01';
|
|
201
|
+
envContent.PREMIUM_PRICE = envContent.PREMIUM_PRICE || '0.001';
|
|
202
|
+
|
|
203
|
+
// Write .env file
|
|
204
|
+
const envLines = Object.entries(envContent).map(([key, value]) => `${key}=${value}`);
|
|
205
|
+
writeFileSync(envPath, envLines.join('\n') + '\n');
|
|
206
|
+
|
|
207
|
+
log.success('.env file configured');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Setup wallet
|
|
212
|
+
*/
|
|
213
|
+
async function setupWallet() {
|
|
214
|
+
log.info('Setting up Solana wallet...');
|
|
215
|
+
|
|
216
|
+
const defaultWalletPath = path.join(__dirname, 'wallet.json');
|
|
217
|
+
|
|
218
|
+
// Check for existing wallet
|
|
219
|
+
if (existsSync(defaultWalletPath)) {
|
|
220
|
+
const answer = await question(`Wallet exists at ${defaultWalletPath}. Use it? (Y/n): `);
|
|
221
|
+
if (answer.toLowerCase() !== 'n') {
|
|
222
|
+
log.success('Using existing wallet');
|
|
223
|
+
return defaultWalletPath;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Ask if user wants to create new wallet or use existing
|
|
228
|
+
console.log('\nWallet options:');
|
|
229
|
+
console.log('1. Generate new wallet');
|
|
230
|
+
console.log('2. Use existing wallet file');
|
|
231
|
+
|
|
232
|
+
const choice = await question('Choose option (1 or 2): ');
|
|
233
|
+
|
|
234
|
+
if (choice === '2') {
|
|
235
|
+
const walletPath = await question('Enter path to existing wallet.json: ');
|
|
236
|
+
if (!existsSync(walletPath)) {
|
|
237
|
+
throw new Error(`Wallet file not found: ${walletPath}`);
|
|
238
|
+
}
|
|
239
|
+
log.success(`Using wallet at ${walletPath}`);
|
|
240
|
+
return walletPath;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Generate new wallet
|
|
244
|
+
log.info('Generating new wallet...');
|
|
245
|
+
|
|
246
|
+
const keypair = Keypair.generate();
|
|
247
|
+
const secretKey = Array.from(keypair.secretKey);
|
|
248
|
+
|
|
249
|
+
// Save wallet
|
|
250
|
+
writeFileSync(defaultWalletPath, JSON.stringify(secretKey));
|
|
251
|
+
|
|
252
|
+
console.log(`\n${colors.green}${colors.bright}Wallet created!${colors.reset}`);
|
|
253
|
+
console.log(`Address: ${colors.cyan}${keypair.publicKey.toBase58()}${colors.reset}`);
|
|
254
|
+
console.log(`Path: ${defaultWalletPath}`);
|
|
255
|
+
|
|
256
|
+
log.warn('IMPORTANT: Backup this wallet file! It contains your private key.');
|
|
257
|
+
|
|
258
|
+
const answer = await question('\nPress Enter to continue...');
|
|
259
|
+
|
|
260
|
+
return defaultWalletPath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Fund wallet with devnet SOL
|
|
265
|
+
*/
|
|
266
|
+
async function fundWallet(walletPath) {
|
|
267
|
+
log.info('Checking wallet balance...');
|
|
268
|
+
|
|
269
|
+
// Load wallet
|
|
270
|
+
const keypairData = JSON.parse(readFileSync(walletPath, 'utf-8'));
|
|
271
|
+
const secretKey = Uint8Array.from(keypairData);
|
|
272
|
+
const keypair = Keypair.fromSecretKey(secretKey);
|
|
273
|
+
|
|
274
|
+
// Check balance
|
|
275
|
+
const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
|
|
276
|
+
const balance = await connection.getBalance(keypair.publicKey);
|
|
277
|
+
const balanceSOL = balance / LAMPORTS_PER_SOL;
|
|
278
|
+
|
|
279
|
+
console.log(`Current balance: ${balanceSOL} SOL`);
|
|
280
|
+
|
|
281
|
+
if (balanceSOL >= 0.02) {
|
|
282
|
+
log.success('Wallet has sufficient balance');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Request airdrop
|
|
287
|
+
const answer = await question('Request devnet SOL airdrop? (Y/n): ');
|
|
288
|
+
if (answer.toLowerCase() === 'n') {
|
|
289
|
+
log.warn('Skipping airdrop. Make sure you have at least 0.02 SOL to run.');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
log.info('Requesting airdrop (this may take a moment)...');
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const signature = await connection.requestAirdrop(
|
|
297
|
+
keypair.publicKey,
|
|
298
|
+
LAMPORTS_PER_SOL
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Wait for confirmation
|
|
302
|
+
await connection.confirmTransaction(signature, 'confirmed');
|
|
303
|
+
|
|
304
|
+
// Check new balance
|
|
305
|
+
const newBalance = await connection.getBalance(keypair.publicKey);
|
|
306
|
+
const newBalanceSOL = newBalance / LAMPORTS_PER_SOL;
|
|
307
|
+
|
|
308
|
+
log.success(`Airdrop successful! New balance: ${newBalanceSOL} SOL`);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
log.error(`Airdrop failed: ${error.message}`);
|
|
311
|
+
log.info('You can request airdrop manually:');
|
|
312
|
+
console.log(` solana airdrop 1 ${keypair.publicKey.toBase58()} --url devnet`);
|
|
313
|
+
console.log('Or use the faucet: https://faucet.solana.com/');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Global installation
|
|
319
|
+
*/
|
|
320
|
+
async function globalInstallation() {
|
|
321
|
+
const answer = await question('\nInstall globally (allows "decharge-scout" command)? (Y/n): ');
|
|
322
|
+
|
|
323
|
+
if (answer.toLowerCase() === 'n') {
|
|
324
|
+
log.info('Skipping global installation');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
log.info('Installing globally...');
|
|
329
|
+
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
const npm = spawn('npm', ['install', '-g', '.'], {
|
|
332
|
+
cwd: __dirname,
|
|
333
|
+
stdio: 'inherit'
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
npm.on('close', (code) => {
|
|
337
|
+
if (code !== 0) {
|
|
338
|
+
log.warn('Global installation failed (may need sudo)');
|
|
339
|
+
log.info('Try manually: sudo npm install -g .');
|
|
340
|
+
resolve(); // Don't fail setup
|
|
341
|
+
} else {
|
|
342
|
+
log.success('Installed globally');
|
|
343
|
+
resolve();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Show final summary
|
|
351
|
+
*/
|
|
352
|
+
function showFinalSummary(walletPath) {
|
|
353
|
+
console.log('\n' + '='.repeat(60));
|
|
354
|
+
log.title('✅ Setup Complete!');
|
|
355
|
+
|
|
356
|
+
console.log('Your DeCharge Scout is ready to run.\n');
|
|
357
|
+
|
|
358
|
+
console.log(`${colors.bright}Quick Start:${colors.reset}`);
|
|
359
|
+
console.log(` ${colors.cyan}decharge-scout${colors.reset} (if installed globally)`);
|
|
360
|
+
console.log(` ${colors.cyan}node index.js${colors.reset} (run directly)\n`);
|
|
361
|
+
|
|
362
|
+
console.log(`${colors.bright}With options:${colors.reset}`);
|
|
363
|
+
console.log(` decharge-scout --agent-name="MyAgent"`);
|
|
364
|
+
console.log(` decharge-scout --premium\n`);
|
|
365
|
+
|
|
366
|
+
console.log(`${colors.bright}Configuration:${colors.reset}`);
|
|
367
|
+
console.log(` Wallet: ${walletPath}`);
|
|
368
|
+
console.log(` Config: ${path.join(__dirname, '.env')}\n`);
|
|
369
|
+
|
|
370
|
+
console.log(`${colors.bright}Next steps:${colors.reset}`);
|
|
371
|
+
console.log(` 1. ${colors.green}✓${colors.reset} Dependencies installed`);
|
|
372
|
+
console.log(` 2. ${colors.green}✓${colors.reset} Wallet created and funded`);
|
|
373
|
+
console.log(` 3. ${colors.green}✓${colors.reset} Environment configured`);
|
|
374
|
+
console.log(` 4. ${colors.yellow}→${colors.reset} Run the scout!\n`);
|
|
375
|
+
|
|
376
|
+
if (!existsSync(path.join(__dirname, '.env')) || readFileSync(path.join(__dirname, '.env'), 'utf-8').includes('your_eia_api_key_here')) {
|
|
377
|
+
log.warn('Remember to add your EIA_API_KEY to .env');
|
|
378
|
+
console.log(' Get it from: https://www.eia.gov/opendata/register.php\n');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
console.log('='.repeat(60) + '\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Run setup
|
|
385
|
+
main().catch(error => {
|
|
386
|
+
log.error(`Fatal error: ${error.message}`);
|
|
387
|
+
rl.close();
|
|
388
|
+
process.exit(1);
|
|
389
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Energy Data Fetching Module
|
|
3
|
+
*
|
|
4
|
+
* Fetches real-time energy grid data from various APIs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fetch from 'node-fetch';
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
|
|
10
|
+
dotenv.config();
|
|
11
|
+
|
|
12
|
+
const EIA_API_KEY = process.env.EIA_API_KEY;
|
|
13
|
+
const EIA_BASE_URL = 'https://api.eia.gov/v2';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetch energy data from EIA API (ERCOT)
|
|
17
|
+
*/
|
|
18
|
+
export async function fetchEnergyData() {
|
|
19
|
+
if (!EIA_API_KEY || EIA_API_KEY === 'your_eia_api_key_here') {
|
|
20
|
+
throw new Error('EIA_API_KEY not configured');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Calculate time range (last 24 hours to next 24 hours for forecast)
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
27
|
+
const startDate = yesterday.toISOString().split('T')[0] + 'T00';
|
|
28
|
+
const endDate = now.toISOString().split('T')[0] + 'T23';
|
|
29
|
+
|
|
30
|
+
// EIA API endpoint for ERCOT real-time data
|
|
31
|
+
const url = `${EIA_BASE_URL}/electricity/rto/region-data/data/?` +
|
|
32
|
+
`api_key=${EIA_API_KEY}` +
|
|
33
|
+
`&frequency=hourly` +
|
|
34
|
+
`&data[0]=value` +
|
|
35
|
+
`&facets[respondent][]=ERCT` +
|
|
36
|
+
`&facets[type][]=D` + // Demand
|
|
37
|
+
`&start=${startDate}` +
|
|
38
|
+
`&end=${endDate}` +
|
|
39
|
+
`&sort[0][column]=period` +
|
|
40
|
+
`&sort[0][direction]=desc` +
|
|
41
|
+
`&length=48`;
|
|
42
|
+
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
headers: {
|
|
45
|
+
'Accept': 'application/json'
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`EIA API error: ${response.status} ${response.statusText}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
|
|
55
|
+
if (!data.response || !data.response.data || data.response.data.length === 0) {
|
|
56
|
+
throw new Error('No data returned from EIA API');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Transform to our format with simulated pricing
|
|
60
|
+
// EIA provides demand, we'll simulate price based on demand patterns
|
|
61
|
+
const energyData = data.response.data.map((item, index) => {
|
|
62
|
+
const demand = parseFloat(item.value) || 0;
|
|
63
|
+
// Simulate price: higher demand = higher price
|
|
64
|
+
// Base price $0.03-0.10/kWh with demand-based variation
|
|
65
|
+
const basePrice = 0.05;
|
|
66
|
+
const demandFactor = (demand / 50000) * 0.03; // Scale based on typical ERCOT demand
|
|
67
|
+
const timeVariation = Math.sin(index * 0.26) * 0.02; // Time-based variation
|
|
68
|
+
const price = basePrice + demandFactor + timeVariation + (Math.random() * 0.01);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
timestamp: item.period,
|
|
72
|
+
hour: new Date(item.period).getHours(),
|
|
73
|
+
demand: demand,
|
|
74
|
+
price: Math.max(0.03, Math.min(0.15, price)), // Clamp between 3-15 cents
|
|
75
|
+
source: 'EIA-ERCOT'
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return energyData;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(`Failed to fetch EIA data: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Fetch forecast data from Electricity Maps API
|
|
87
|
+
*/
|
|
88
|
+
export async function fetchElectricityMapsData() {
|
|
89
|
+
try {
|
|
90
|
+
const url = 'https://api.electricitymaps.com/v3/power-breakdown/latest?zone=US-TEX-ERCO';
|
|
91
|
+
|
|
92
|
+
const response = await fetch(url, {
|
|
93
|
+
headers: {
|
|
94
|
+
'Accept': 'application/json'
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
// If auth required or rate limited, generate mock data
|
|
100
|
+
if (response.status === 401 || response.status === 429) {
|
|
101
|
+
console.log('Electricity Maps requires auth, using mock forecast data');
|
|
102
|
+
return generateMockForecastData();
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Electricity Maps API error: ${response.status}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const data = await response.json();
|
|
108
|
+
|
|
109
|
+
// Generate 24-hour forecast based on current data
|
|
110
|
+
const forecastData = [];
|
|
111
|
+
const currentHour = new Date().getHours();
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < 24; i++) {
|
|
114
|
+
const hour = (currentHour + i) % 24;
|
|
115
|
+
const timestamp = new Date(Date.now() + i * 60 * 60 * 1000).toISOString();
|
|
116
|
+
|
|
117
|
+
// Simulate price variation based on typical daily patterns
|
|
118
|
+
const isOffPeak = hour >= 22 || hour <= 6; // 10 PM - 6 AM
|
|
119
|
+
const isPeak = hour >= 16 && hour <= 20; // 4 PM - 8 PM
|
|
120
|
+
const basePrice = 0.06;
|
|
121
|
+
const peakMultiplier = isPeak ? 1.5 : 1.0;
|
|
122
|
+
const offPeakMultiplier = isOffPeak ? 0.7 : 1.0;
|
|
123
|
+
const randomVariation = (Math.random() - 0.5) * 0.02;
|
|
124
|
+
|
|
125
|
+
const price = basePrice * peakMultiplier * offPeakMultiplier + randomVariation;
|
|
126
|
+
|
|
127
|
+
forecastData.push({
|
|
128
|
+
timestamp,
|
|
129
|
+
hour,
|
|
130
|
+
demand: 45000 + (isPeak ? 15000 : 0) - (isOffPeak ? 10000 : 0),
|
|
131
|
+
price: Math.max(0.03, Math.min(0.12, price)),
|
|
132
|
+
source: 'ElectricityMaps-Forecast'
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return forecastData;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(`Failed to fetch Electricity Maps data: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate mock forecast data as fallback
|
|
144
|
+
*/
|
|
145
|
+
function generateMockForecastData() {
|
|
146
|
+
const forecastData = [];
|
|
147
|
+
const currentHour = new Date().getHours();
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < 24; i++) {
|
|
150
|
+
const hour = (currentHour + i) % 24;
|
|
151
|
+
const timestamp = new Date(Date.now() + i * 60 * 60 * 1000).toISOString();
|
|
152
|
+
|
|
153
|
+
// Realistic daily price pattern
|
|
154
|
+
const isOffPeak = hour >= 22 || hour <= 6;
|
|
155
|
+
const isPeak = hour >= 16 && hour <= 20;
|
|
156
|
+
const isMidDay = hour >= 10 && hour <= 14;
|
|
157
|
+
|
|
158
|
+
let price = 0.05; // Base price
|
|
159
|
+
|
|
160
|
+
if (isPeak) {
|
|
161
|
+
price = 0.09 + Math.random() * 0.03; // Peak hours: 9-12 cents
|
|
162
|
+
} else if (isOffPeak) {
|
|
163
|
+
price = 0.03 + Math.random() * 0.02; // Off-peak: 3-5 cents
|
|
164
|
+
} else if (isMidDay) {
|
|
165
|
+
price = 0.06 + Math.random() * 0.02; // Mid-day: 6-8 cents
|
|
166
|
+
} else {
|
|
167
|
+
price = 0.05 + Math.random() * 0.02; // Normal: 5-7 cents
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
forecastData.push({
|
|
171
|
+
timestamp,
|
|
172
|
+
hour,
|
|
173
|
+
demand: 40000 + (isPeak ? 20000 : 0) - (isOffPeak ? 15000 : 0),
|
|
174
|
+
price: parseFloat(price.toFixed(4)),
|
|
175
|
+
source: 'Mock-Data'
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return forecastData;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fetch premium forecast data (for x402 integration)
|
|
184
|
+
*/
|
|
185
|
+
export async function fetchPremiumForecastData() {
|
|
186
|
+
// In a real implementation, this would call a premium API endpoint
|
|
187
|
+
// For demo, return enhanced mock data with more detail
|
|
188
|
+
|
|
189
|
+
const premiumData = generateMockForecastData().map(item => ({
|
|
190
|
+
...item,
|
|
191
|
+
confidence: 0.85 + Math.random() * 0.15,
|
|
192
|
+
carbonIntensity: 300 + Math.random() * 200,
|
|
193
|
+
renewablePercentage: 20 + Math.random() * 30,
|
|
194
|
+
premium: true
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
return premiumData;
|
|
198
|
+
}
|