diff --git a/agents/gas-estimator/.env.example b/agents/gas-estimator/.env.example new file mode 100644 index 0000000..d3fe0d2 --- /dev/null +++ b/agents/gas-estimator/.env.example @@ -0,0 +1,12 @@ +# Gas Estimation Agent Configuration +PORT=3007 +GITHUB_HANDLE=dominusaxis +OWNER_WALLET=0xYourWalletAddress +AGENT_URL=http://localhost:3007 +PAYPOL_MARKETPLACE_URL=http://localhost:3000 + +# Optional: Custom RPC endpoints (defaults to public RPCs) +# ETH_RPC=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY +# ARB_RPC=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY +# BASE_RPC=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY +# TEMPO_RPC=https://rpc.tempo.xyz diff --git a/agents/gas-estimator/README.md b/agents/gas-estimator/README.md new file mode 100644 index 0000000..f09f2da --- /dev/null +++ b/agents/gas-estimator/README.md @@ -0,0 +1,39 @@ +# Gas Estimation Agent + +Compares real-time gas costs across **Tempo L1, Ethereum, Arbitrum, and Base** and recommends the cheapest chain for your transaction. + +## Features + +- Queries gas prices from 4 chains simultaneously +- Returns costs in gwei and USD +- Estimates for: simple transfer, ERC-20 transfer, contract deploy, swap +- 15-second TTL cache to avoid rate limiting +- Graceful RPC failure handling with fallbacks +- Registers on PayPol marketplace via SDK + +## Quick Start + +```bash +cp .env.example .env +npm install +npm run dev # Start the agent +npm run register # Register on marketplace +``` + +## Example Request + +```bash +curl -X POST http://localhost:3007/execute \ + -H "Content-Type: application/json" \ + -d '{ + "jobId": "test-1", + "agentId": "gas-estimator", + "prompt": "Compare gas costs for an ERC-20 transfer", + "callerWallet": "0xabc123", + "timestamp": 1234567890 + }' +``` + +## Bounty + +Built for [PayPol Issue #7](https://github.com/PayPol-Foundation/paypol-protocol/issues/7). diff --git a/agents/gas-estimator/package.json b/agents/gas-estimator/package.json new file mode 100644 index 0000000..752abab --- /dev/null +++ b/agents/gas-estimator/package.json @@ -0,0 +1,21 @@ +{ + "name": "paypol-gas-estimator", + "version": "1.0.0", + "description": "Gas Estimation Agent — compares gas costs across Tempo L1, Ethereum, Arbitrum, and Base", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "dev": "npx ts-node src/index.ts", + "start": "node dist/index.js", + "register": "npx ts-node src/register.ts" + }, + "dependencies": { + "paypol-sdk": "workspace:^", + "dotenv": "^16.4.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + } +} diff --git a/agents/gas-estimator/src/index.ts b/agents/gas-estimator/src/index.ts new file mode 100644 index 0000000..d90ec65 --- /dev/null +++ b/agents/gas-estimator/src/index.ts @@ -0,0 +1,272 @@ +/** + * PayPol Gas Estimation Agent + * + * Compares real-time gas costs across Tempo L1, Ethereum, Arbitrum, and Base. + * Recommends the cheapest chain and timing for common operations. + * + * Bounty: https://github.com/PayPol-Foundation/paypol-protocol/issues/7 + */ + +import 'dotenv/config'; +import { PayPolAgent, JobRequest, JobResult } from 'paypol-sdk'; + +// ── Configuration ──────────────────────────────────────── + +const RPC_ENDPOINTS: Record = { + 'Ethereum': process.env.ETH_RPC ?? 'https://eth.llamarpc.com', + 'Arbitrum': process.env.ARB_RPC ?? 'https://arb1.arbitrum.io/rpc', + 'Base': process.env.BASE_RPC ?? 'https://mainnet.base.org', + 'Tempo L1': process.env.TEMPO_RPC ?? 'https://rpc.tempo.xyz', +}; + +// ETH price for USD conversion (updated on each request) +let ethPriceUsd = 3500; + +// Gas limits for common operations +const GAS_LIMITS: Record = { + 'simple-transfer': 21_000, + 'erc20-transfer': 65_000, + 'contract-deploy': 1_500_000, + 'swap': 150_000, +}; + +// Average block times per chain +const BLOCK_TIMES: Record = { + 'Ethereum': '12s', + 'Arbitrum': '0.25s', + 'Base': '2s', + 'Tempo L1': '2s', +}; + +// Cache for gas prices (TTL-based) +interface CacheEntry { + data: GasPrice; + expiresAt: number; +} + +interface GasPrice { + chain: string; + gasPriceGwei: number; + baseFeeGwei: number; + priorityFeeGwei: number; +} + +const cache = new Map(); +const CACHE_TTL_MS = 15_000; // 15 seconds default + +// ── Gas Price Fetching ─────────────────────────────────── + +async function fetchGasPrice(chain: string, rpcUrl: string): Promise { + // Check cache first + const cached = cache.get(chain); + if (cached && Date.now() < cached.expiresAt) { + return cached.data; + } + + try { + // Fetch gas price via eth_gasPrice + const gasPriceRes = await fetch(rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_gasPrice', params: [], id: 1 }), + signal: AbortSignal.timeout(5000), + }); + + const gasPriceData = await gasPriceRes.json(); + const gasPriceWei = parseInt(gasPriceData.result || '0x0', 16); + const gasPriceGwei = gasPriceWei / 1e9; + + // Try to get base fee and priority fee (EIP-1559) + let baseFeeGwei = gasPriceGwei; + let priorityFeeGwei = 0; + + try { + const feeRes = await fetch(rpcUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_feeHistory', + params: ['0x1', 'latest', [50]], + id: 2, + }), + signal: AbortSignal.timeout(5000), + }); + + const feeData = await feeRes.json(); + if (feeData.result?.baseFeePerGas?.[0]) { + baseFeeGwei = parseInt(feeData.result.baseFeePerGas[0], 16) / 1e9; + } + if (feeData.result?.reward?.[0]?.[0]) { + priorityFeeGwei = parseInt(feeData.result.reward[0][0], 16) / 1e9; + } + } catch { + // EIP-1559 not supported on this chain, use legacy gas price + } + + const result: GasPrice = { + chain, + gasPriceGwei: Math.round(gasPriceGwei * 1000) / 1000, + baseFeeGwei: Math.round(baseFeeGwei * 1000) / 1000, + priorityFeeGwei: Math.round(priorityFeeGwei * 1000) / 1000, + }; + + // Cache the result + cache.set(chain, { data: result, expiresAt: Date.now() + CACHE_TTL_MS }); + return result; + } catch (err) { + // Return zero gas price on failure (chain may be free or unreachable) + console.warn(`[gas-estimator] Failed to fetch gas for ${chain}: ${err}`); + return { chain, gasPriceGwei: 0, baseFeeGwei: 0, priorityFeeGwei: 0 }; + } +} + +async function fetchEthPrice(): Promise { + try { + const res = await fetch( + 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd', + { signal: AbortSignal.timeout(5000) }, + ); + const data = await res.json(); + return data.ethereum?.usd ?? ethPriceUsd; + } catch { + return ethPriceUsd; + } +} + +// ── Cost Calculation ───────────────────────────────────── + +interface ChainEstimate { + chain: string; + gasPrice: string; + cost: string; + speed: string; + gasPriceGwei: number; + costUsd: number; +} + +function calculateCost( + gasPrice: GasPrice, + gasLimit: number, + ethPrice: number, +): ChainEstimate { + const totalGwei = gasPrice.gasPriceGwei; + const costEth = (totalGwei * gasLimit) / 1e9; + const costUsd = costEth * ethPrice; + + return { + chain: gasPrice.chain, + gasPrice: `${totalGwei.toFixed(3)} gwei`, + cost: costUsd < 0.01 ? '$0.00' : `$${costUsd.toFixed(2)}`, + speed: BLOCK_TIMES[gasPrice.chain] ?? 'unknown', + gasPriceGwei: totalGwei, + costUsd, + }; +} + +// ── Agent Setup ────────────────────────────────────────── + +const agent = new PayPolAgent({ + id: 'gas-estimator', + name: 'Gas Estimation Agent', + description: 'Compares real-time gas costs across Tempo L1, Ethereum, Arbitrum, and Base. Recommends the cheapest chain for your transaction.', + category: 'automation', + version: '1.0.0', + price: 1, + capabilities: ['gas-estimation', 'multi-chain', 'cost-optimization'], + author: process.env.GITHUB_HANDLE ?? 'dominusaxis', +}); + +// ── Job Handler ────────────────────────────────────────── + +agent.onJob(async (job: JobRequest): Promise => { + const start = Date.now(); + console.log(`[gas-estimator] Job ${job.jobId}: ${job.prompt}`); + + try { + // Determine operation type from prompt or payload + const operation = detectOperation(job.prompt, job.payload); + const gasLimit = GAS_LIMITS[operation] ?? GAS_LIMITS['simple-transfer']; + + // Fetch gas prices from all chains concurrently + const [prices, ethPrice] = await Promise.all([ + Promise.all( + Object.entries(RPC_ENDPOINTS).map(([chain, rpc]) => fetchGasPrice(chain, rpc)), + ), + fetchEthPrice(), + ]); + + ethPriceUsd = ethPrice; + + // Calculate cost estimates for each chain + const estimates = prices + .map((p) => calculateCost(p, gasLimit, ethPrice)) + .sort((a, b) => a.costUsd - b.costUsd); + + // Find cheapest + const cheapest = estimates[0]; + const recommendation = + cheapest.costUsd === 0 + ? `${cheapest.chain} has zero gas fees with fast finality.` + : `${cheapest.chain} is cheapest at ${cheapest.cost} (${cheapest.speed} finality).`; + + return { + jobId: job.jobId, + agentId: 'gas-estimator', + status: 'success', + result: { + operation: formatOperation(operation), + gasLimit, + ethPriceUsd: ethPrice, + estimates: estimates.map((e) => ({ + chain: e.chain, + gasPrice: e.gasPrice, + cost: e.cost, + speed: e.speed, + })), + recommendation, + cachedAt: new Date().toISOString(), + }, + executionTimeMs: Date.now() - start, + timestamp: Date.now(), + }; + } catch (err) { + return { + jobId: job.jobId, + agentId: 'gas-estimator', + status: 'error', + error: `Gas estimation failed: ${err}`, + executionTimeMs: Date.now() - start, + timestamp: Date.now(), + }; + } +}); + +// ── Helpers ────────────────────────────────────────────── + +function detectOperation(prompt: string, payload?: Record): string { + const p = prompt.toLowerCase(); + if (payload?.operation && typeof payload.operation === 'string') { + const op = payload.operation.toLowerCase().replace(/\s+/g, '-'); + if (op in GAS_LIMITS) return op; + } + if (p.includes('deploy') || p.includes('contract')) return 'contract-deploy'; + if (p.includes('swap') || p.includes('exchange') || p.includes('trade')) return 'swap'; + if (p.includes('erc20') || p.includes('token') || p.includes('transfer token')) return 'erc20-transfer'; + return 'simple-transfer'; +} + +function formatOperation(op: string): string { + return op + .split('-') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +// ── Start ──────────────────────────────────────────────── + +const port = parseInt(process.env.PORT ?? '3007', 10); +agent.listen(port, () => { + console.log(`[gas-estimator] Ready on port ${port}`); + console.log(`[gas-estimator] Chains: ${Object.keys(RPC_ENDPOINTS).join(', ')}`); +}); diff --git a/agents/gas-estimator/src/register.ts b/agents/gas-estimator/src/register.ts new file mode 100644 index 0000000..8815438 --- /dev/null +++ b/agents/gas-estimator/src/register.ts @@ -0,0 +1,21 @@ +import 'dotenv/config'; +import { registerAgent } from 'paypol-sdk'; + +async function main() { + const result = await registerAgent({ + id: 'gas-estimator', + name: 'Gas Estimation Agent', + description: 'Compares real-time gas costs across chains and recommends the cheapest option.', + category: 'automation', + version: '1.0.0', + price: 1, + capabilities: ['gas-estimation', 'multi-chain', 'cost-optimization'], + author: process.env.GITHUB_HANDLE ?? 'dominusaxis', + webhookUrl: process.env.AGENT_URL ?? 'http://localhost:3007', + ownerWallet: process.env.OWNER_WALLET ?? '0x0000000000000000000000000000000000000000', + }); + + console.log('Registered:', result); +} + +main().catch(console.error); diff --git a/agents/gas-estimator/tsconfig.json b/agents/gas-estimator/tsconfig.json new file mode 100644 index 0000000..08a5275 --- /dev/null +++ b/agents/gas-estimator/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "declaration": true + }, + "include": ["src/**/*"] +}