Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions agents/gas-estimator/.env.example
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions agents/gas-estimator/README.md
Original file line number Diff line number Diff line change
@@ -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).
21 changes: 21 additions & 0 deletions agents/gas-estimator/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
272 changes: 272 additions & 0 deletions agents/gas-estimator/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
'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<string, number> = {
'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<string, string> = {
'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<string, CacheEntry>();
const CACHE_TTL_MS = 15_000; // 15 seconds default

// ── Gas Price Fetching ───────────────────────────────────

async function fetchGasPrice(chain: string, rpcUrl: string): Promise<GasPrice> {
// 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<number> {
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<JobResult> => {
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, unknown>): 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(', ')}`);
});
21 changes: 21 additions & 0 deletions agents/gas-estimator/src/register.ts
Original file line number Diff line number Diff line change
@@ -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);
13 changes: 13 additions & 0 deletions agents/gas-estimator/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true
},
"include": ["src/**/*"]
}