v2.1.1: Stats endpoint, improved payment flow, test coverage
- /stats endpoint with real-time payment analytics and session tracking - Multi-chain payment support (Base + SKALE Europa) - SIWx session management for repeat access - Updated README with comprehensive feature docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dde6d1252e
commit
eb346e535f
20
README.md
20
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
**Pay-per-request AI agent services via A2A protocol + x402 V2 micropayments**
|
||||
|
||||
An A2A-compliant agent server that exposes screenshot, PDF, and document generation services with x402 V2 cryptocurrency micropayments on Base + SKALE networks. Features SIWx session authentication for repeat access.
|
||||
An A2A-compliant agent server that exposes screenshot, PDF, and document generation services with x402 V2 cryptocurrency micropayments on Base + SKALE Europa (gasless) networks. Features SIWx session authentication for repeat access.
|
||||
|
||||
Built by [OpSpawn](https://opspawn.com) for the [SF Agentic Commerce x402 Hackathon](https://dorahacks.io/hackathon/x402).
|
||||
|
||||
@ -78,7 +78,8 @@ Payment requirements are returned via A2A task metadata using x402 V2 with CAIP-
|
||||
"price": "$0.01"
|
||||
}, {
|
||||
"scheme": "exact",
|
||||
"network": "eip155:324705682",
|
||||
"network": "eip155:2046399126",
|
||||
"asset": "0x5F795bb52dAC3085f578f4877D450e2929D2F13d",
|
||||
"gasless": true
|
||||
}]
|
||||
}
|
||||
@ -163,18 +164,18 @@ const { result: task } = await r1.json();
|
||||
└─────────────┘ └──────────────┘ └──────────┘
|
||||
│
|
||||
x402 Payment
|
||||
│
|
||||
┌──────────────┐
|
||||
│ Base Network │
|
||||
│ (USDC) │
|
||||
└──────────────┘
|
||||
┌────┴────┐
|
||||
┌──────────┐ ┌──────────────┐
|
||||
│ Base │ │ SKALE Europa │
|
||||
│ (USDC) │ │ (USDC, $0) │
|
||||
└──────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Runtime**: Node.js 22
|
||||
- **Protocol**: A2A v0.3 (JSON-RPC 2.0 over HTTP)
|
||||
- **Payments**: x402 V2 (SDK v2.3.0) on Base + SKALE (USDC)
|
||||
- **Payments**: x402 V2 (SDK v2.3.0) on Base + SKALE Europa (USDC, gasless)
|
||||
- **Auth**: SIWx (CAIP-122 wallet sessions)
|
||||
- **Backend**: Express 5
|
||||
- **Facilitator**: PayAI (facilitator.payai.network)
|
||||
@ -187,13 +188,14 @@ npm start &
|
||||
npm test
|
||||
```
|
||||
|
||||
19 tests covering:
|
||||
22 tests covering:
|
||||
- Health check and agent card discovery
|
||||
- x402 service catalog
|
||||
- Free skill execution (markdown → HTML)
|
||||
- Paid skill payment requirements (screenshot, PDF)
|
||||
- Payment submission and service delivery
|
||||
- Task lifecycle (get, cancel)
|
||||
- SKALE Europa chain ID, USDC address, and gasless flag validation
|
||||
- Error handling (invalid requests, unknown methods)
|
||||
|
||||
## Configuration
|
||||
|
||||
@ -36,7 +36,7 @@ The agent economy needs a standard way for agents to pay each other for services
|
||||
### Technical Stack
|
||||
- **Runtime**: Node.js 22 + Express.js
|
||||
- **Protocols**: A2A v0.3, x402 V2 protocol
|
||||
- **Payments**: USDC on Base (eip155:8453) and SKALE (eip155:324705682, gasless)
|
||||
- **Payments**: USDC on Base (eip155:8453) and SKALE Europa (eip155:2046399126, gasless, zero gas fees)
|
||||
- **Auth**: SIWx (CAIP-122 wallet authentication for sessions)
|
||||
- **Facilitator**: PayAI Network (facilitator.payai.network)
|
||||
- **Infrastructure**: Ubuntu VM, Cloudflare Tunnel, nginx reverse proxy
|
||||
|
||||
167
server.mjs
167
server.mjs
@ -7,7 +7,7 @@
|
||||
* Architecture:
|
||||
* - A2A protocol v0.3 for agent-to-agent communication (JSON-RPC over HTTP)
|
||||
* - x402 V2 protocol for payment (USDC on Base, gasless on SKALE)
|
||||
* - CAIP-2 network identifiers (eip155:8453, eip155:324705682)
|
||||
* - CAIP-2 network identifiers (eip155:8453, eip155:2046399126)
|
||||
* - Express HTTP server with web dashboard
|
||||
*
|
||||
* Built by OpSpawn for the SF Agentic Commerce x402 Hackathon
|
||||
@ -15,6 +15,9 @@
|
||||
|
||||
import express from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
// === Configuration ===
|
||||
const PORT = parseInt(process.env.PORT || '4002', 10);
|
||||
@ -33,12 +36,60 @@ const NETWORKS = {
|
||||
};
|
||||
const DEFAULT_NETWORK = NETWORKS.base;
|
||||
|
||||
// === Persistence ===
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const STATS_FILE = join(__dirname, 'stats.json');
|
||||
|
||||
function loadStats() {
|
||||
try {
|
||||
if (existsSync(STATS_FILE)) {
|
||||
const raw = readFileSync(STATS_FILE, 'utf8');
|
||||
if (!raw.trim()) {
|
||||
console.log('[stats] Stats file is empty, using defaults');
|
||||
return { paymentLog: [], siwxSessions: {}, totalTasks: 0, startedAt: new Date().toISOString() };
|
||||
}
|
||||
const data = JSON.parse(raw);
|
||||
console.log(`[stats] Loaded ${data.paymentLog?.length || 0} payments, ${Object.keys(data.siwxSessions || {}).length} sessions`);
|
||||
return data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[stats] Failed to load stats file:', e.message);
|
||||
console.error('[stats] Starting with fresh state');
|
||||
}
|
||||
return { paymentLog: [], siwxSessions: {}, totalTasks: 0, startedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
function saveStats() {
|
||||
try {
|
||||
const sessions = {};
|
||||
for (const [wallet, data] of siwxSessions.entries()) {
|
||||
sessions[wallet] = { skills: [...data.paidSkills], lastPayment: data.lastPayment };
|
||||
}
|
||||
const payload = JSON.stringify({
|
||||
paymentLog, siwxSessions: sessions,
|
||||
totalTasks: totalTaskCount, startedAt: persistedStats.startedAt,
|
||||
savedAt: new Date().toISOString(),
|
||||
}, null, 2);
|
||||
writeFileSync(STATS_FILE, payload);
|
||||
} catch (e) {
|
||||
console.error('[stats] Failed to save:', e.message, '— data in memory only');
|
||||
}
|
||||
}
|
||||
|
||||
const persistedStats = loadStats();
|
||||
|
||||
// === State ===
|
||||
const tasks = new Map();
|
||||
const paymentLog = [];
|
||||
const paymentLog = persistedStats.paymentLog || [];
|
||||
let totalTaskCount = persistedStats.totalTasks || 0;
|
||||
|
||||
// === SIWx session store (in-memory) ===
|
||||
// === SIWx session store ===
|
||||
const siwxSessions = new Map(); // wallet address -> { paidSkills: Set, lastPayment: timestamp }
|
||||
// Restore persisted sessions
|
||||
for (const [wallet, data] of Object.entries(persistedStats.siwxSessions || {})) {
|
||||
siwxSessions.set(wallet, { paidSkills: new Set(data.skills || []), lastPayment: data.lastPayment });
|
||||
}
|
||||
console.log(`[stats] Loaded: ${paymentLog.length} payments, ${siwxSessions.size} sessions, ${totalTaskCount} total tasks`);
|
||||
|
||||
function recordSiwxPayment(walletAddress, skill) {
|
||||
const normalized = walletAddress.toLowerCase();
|
||||
@ -63,7 +114,7 @@ const agentCard = {
|
||||
description: 'AI agent providing screenshot, PDF, and document generation services via x402 V2 micropayments on Base + SKALE Europa (gasless). Pay per request with USDC. Supports SIWx session-based auth for repeat access.',
|
||||
url: `${PUBLIC_URL}/`,
|
||||
provider: { organization: 'OpSpawn', url: 'https://opspawn.com' },
|
||||
version: '2.0.0',
|
||||
version: '2.1.0',
|
||||
protocolVersion: '0.3.0',
|
||||
capabilities: {
|
||||
streaming: false,
|
||||
@ -126,6 +177,7 @@ function createTask(id, contextId, state, message) {
|
||||
history: [], artifacts: [], metadata: {},
|
||||
};
|
||||
tasks.set(id, task);
|
||||
totalTaskCount++;
|
||||
return task;
|
||||
}
|
||||
|
||||
@ -152,9 +204,14 @@ function parseRequest(text) {
|
||||
}
|
||||
|
||||
// === Service handlers ===
|
||||
const SNAPAPI_TIMEOUT = 30000; // 30s timeout for SnapAPI calls
|
||||
|
||||
async function handleScreenshot(url) {
|
||||
const params = new URLSearchParams({ url, format: 'png', width: '1280', height: '800' });
|
||||
const resp = await fetch(`${SNAPAPI_URL}/api/capture?${params}`, { headers: { 'X-API-Key': SNAPAPI_KEY } });
|
||||
const resp = await fetch(`${SNAPAPI_URL}/api/capture?${params}`, {
|
||||
headers: { 'X-API-Key': SNAPAPI_KEY },
|
||||
signal: AbortSignal.timeout(SNAPAPI_TIMEOUT),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`SnapAPI error: ${resp.status}`);
|
||||
const buffer = Buffer.from(await resp.arrayBuffer());
|
||||
return {
|
||||
@ -170,6 +227,7 @@ async function handleMarkdownToPdf(markdown) {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-API-Key': SNAPAPI_KEY },
|
||||
body: JSON.stringify({ markdown }),
|
||||
signal: AbortSignal.timeout(SNAPAPI_TIMEOUT),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`SnapAPI error: ${resp.status}`);
|
||||
const buffer = Buffer.from(await resp.arrayBuffer());
|
||||
@ -186,6 +244,7 @@ async function handleMarkdownToHtml(markdown) {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ markdown, theme: 'light' }),
|
||||
signal: AbortSignal.timeout(SNAPAPI_TIMEOUT),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`SnapAPI error: ${resp.status}`);
|
||||
const html = await resp.text();
|
||||
@ -274,7 +333,7 @@ async function handleMessageSend(rpcId, params, res) {
|
||||
const siwxWallet = message.metadata?.['x402.siwx.wallet'];
|
||||
if (siwxWallet && hasSiwxAccess(siwxWallet, request.skill)) {
|
||||
console.log(`[siwx] Session access granted for ${siwxWallet} -> ${request.skill}`);
|
||||
paymentLog.push({ type: 'siwx-access', taskId, skill: request.skill, wallet: siwxWallet, timestamp: new Date().toISOString() });
|
||||
paymentLog.push({ type: 'siwx-access', taskId, skill: request.skill, wallet: siwxWallet, network: null, timestamp: new Date().toISOString() });
|
||||
return handleFreeExecution(rpcId, taskId, contextId, request, message, res);
|
||||
}
|
||||
|
||||
@ -299,7 +358,7 @@ async function handleMessageSend(rpcId, params, res) {
|
||||
task.metadata['x402.skill'] = request.skill;
|
||||
task.metadata['x402.version'] = '2.0';
|
||||
|
||||
paymentLog.push({ type: 'payment-required', taskId, skill: request.skill, amount: payReq.accepts[0].price, timestamp: new Date().toISOString() });
|
||||
paymentLog.push({ type: 'payment-required', taskId, skill: request.skill, amount: payReq.accepts[0].price, network: null, timestamp: new Date().toISOString() });
|
||||
return res.json({ jsonrpc: '2.0', id: rpcId, result: task });
|
||||
}
|
||||
|
||||
@ -332,7 +391,8 @@ async function handleFreeExecution(rpcId, taskId, contextId, request, message, r
|
||||
async function handlePaidExecution(rpcId, taskId, contextId, request, paymentPayload, message, res) {
|
||||
console.log(`[x402-v2] Payment received for ${request.skill}`);
|
||||
const payerWallet = paymentPayload?.from || message.metadata?.['x402.payer'] || 'unknown';
|
||||
paymentLog.push({ type: 'payment-received', taskId, skill: request.skill, wallet: payerWallet, timestamp: new Date().toISOString() });
|
||||
const paymentNetwork = paymentPayload?.network || message.metadata?.['x402.network'] || 'eip155:8453';
|
||||
paymentLog.push({ type: 'payment-received', taskId, skill: request.skill, wallet: payerWallet, network: paymentNetwork, timestamp: new Date().toISOString() });
|
||||
|
||||
// Record SIWx session so the payer can re-access without paying again
|
||||
if (payerWallet !== 'unknown') {
|
||||
@ -350,7 +410,8 @@ async function handlePaidExecution(rpcId, taskId, contextId, request, paymentPay
|
||||
else result = await handleMarkdownToHtml(request.markdown || '# Hello');
|
||||
|
||||
const txHash = `0x${uuidv4().replace(/-/g, '')}`;
|
||||
paymentLog.push({ type: 'payment-settled', taskId, skill: request.skill, txHash, wallet: payerWallet, timestamp: new Date().toISOString() });
|
||||
paymentLog.push({ type: 'payment-settled', taskId, skill: request.skill, txHash, wallet: payerWallet, network: paymentNetwork, timestamp: new Date().toISOString() });
|
||||
saveStats();
|
||||
|
||||
updateTask(taskId, 'completed', {
|
||||
kind: 'message', role: 'agent', messageId: uuidv4(), parts: result.parts, taskId, contextId,
|
||||
@ -404,7 +465,7 @@ app.get('/api/info', (req, res) => res.json({
|
||||
services: { screenshot: '$0.01', 'markdown-to-pdf': '$0.005', 'markdown-to-html': 'free' },
|
||||
},
|
||||
stats: {
|
||||
payments: paymentLog.length, tasks: tasks.size, uptime: process.uptime(),
|
||||
payments: paymentLog.length, tasks: totalTaskCount, tasksThisSession: tasks.size, uptime: process.uptime(),
|
||||
siwxSessions: siwxSessions.size,
|
||||
paymentsByType: {
|
||||
required: paymentLog.filter(p => p.type === 'payment-required').length,
|
||||
@ -423,7 +484,7 @@ app.get('/api/siwx', (req, res) => {
|
||||
res.json({ sessions, total: sessions.length });
|
||||
});
|
||||
app.get('/x402', (req, res) => res.json({
|
||||
service: 'OpSpawn A2A x402 Gateway', version: '2.0.0',
|
||||
service: 'OpSpawn A2A x402 Gateway', version: '2.1.0',
|
||||
description: 'A2A-compliant agent with x402 V2 micropayment services on Base + SKALE Europa (gasless)',
|
||||
provider: { name: 'OpSpawn', url: 'https://opspawn.com' },
|
||||
protocols: {
|
||||
@ -470,18 +531,78 @@ app.get('/stats', (req, res) => {
|
||||
settled: paymentLog.filter(p => p.type === 'payment-settled').length,
|
||||
siwxAccess: paymentLog.filter(p => p.type === 'siwx-access').length,
|
||||
};
|
||||
const allTasks = [...tasks.values()];
|
||||
const completed = allTasks.filter(t => t.status.state === 'completed').length;
|
||||
const failed = allTasks.filter(t => t.status.state === 'failed').length;
|
||||
res.json({
|
||||
agent: { name: 'OpSpawn Screenshot Agent', version: '2.0.0', url: PUBLIC_URL },
|
||||
agent: { name: 'OpSpawn Screenshot Agent', version: '2.1.0', url: PUBLIC_URL },
|
||||
uptime: { seconds: Math.round(uptime), human: formatUptime(uptime) },
|
||||
tasks: { total: tasks.size, completed: [...tasks.values()].filter(t => t.status.state === 'completed').length, failed: [...tasks.values()].filter(t => t.status.state === 'failed').length },
|
||||
payments: { total: paymentLog.length, byType, revenue: { currency: 'USDC', estimated: (byType.settled * 0.01).toFixed(4) } },
|
||||
sessions: { siwx: siwxSessions.size },
|
||||
tasks: {
|
||||
total: totalTaskCount, thisSession: tasks.size, completed, failed,
|
||||
errorRate: tasks.size > 0 ? (failed / tasks.size * 100).toFixed(1) + '%' : '0%',
|
||||
},
|
||||
payments: {
|
||||
total: paymentLog.length, byType,
|
||||
revenue: calculateDetailedRevenue(),
|
||||
},
|
||||
sessions: {
|
||||
siwx: siwxSessions.size,
|
||||
reuseCount: byType.siwxAccess,
|
||||
savingsEstimate: (byType.siwxAccess * 0.01).toFixed(4),
|
||||
},
|
||||
services: agentCard.skills.map(s => ({ id: s.id, name: s.name, price: s.id === 'screenshot' ? '$0.01' : s.id === 'markdown-to-pdf' ? '$0.005' : 'free' })),
|
||||
networks: Object.values(NETWORKS).map(n => ({ network: n.caip2, name: n.name, gasless: n.gasless || false })),
|
||||
recentActivity: paymentLog.slice(-10).reverse().map(p => ({
|
||||
type: p.type, skill: p.skill, network: p.network, timestamp: p.timestamp,
|
||||
})),
|
||||
protocol: {
|
||||
a2a: { version: '0.3.0', methods: ['message/send', 'tasks/get', 'tasks/cancel'] },
|
||||
x402: { version: '2.0', features: ['siwx', 'payment-identifier', 'bazaar-discovery', 'multi-chain'] },
|
||||
},
|
||||
timestamp: now,
|
||||
});
|
||||
});
|
||||
|
||||
function calculateDetailedRevenue() {
|
||||
const bySkill = {};
|
||||
const byNetwork = {};
|
||||
const skillCounts = {};
|
||||
let total = 0;
|
||||
let settledCount = 0;
|
||||
const timestamps = [];
|
||||
for (const p of paymentLog) {
|
||||
if (p.type === 'payment-settled') {
|
||||
const amount = p.skill === 'screenshot' ? 0.01 : p.skill === 'markdown-to-pdf' ? 0.005 : 0;
|
||||
total += amount;
|
||||
settledCount++;
|
||||
bySkill[p.skill] = (bySkill[p.skill] || 0) + amount;
|
||||
skillCounts[p.skill] = (skillCounts[p.skill] || 0) + 1;
|
||||
const net = p.network || 'eip155:8453';
|
||||
byNetwork[net] = (byNetwork[net] || 0) + amount;
|
||||
if (p.timestamp) timestamps.push(new Date(p.timestamp).getTime());
|
||||
}
|
||||
}
|
||||
// Calculate average time between payments
|
||||
let avgInterval = null;
|
||||
if (timestamps.length > 1) {
|
||||
timestamps.sort((a, b) => a - b);
|
||||
const intervals = [];
|
||||
for (let i = 1; i < timestamps.length; i++) intervals.push(timestamps[i] - timestamps[i - 1]);
|
||||
avgInterval = Math.round(intervals.reduce((a, b) => a + b, 0) / intervals.length / 1000);
|
||||
}
|
||||
return {
|
||||
currency: 'USDC',
|
||||
total: total.toFixed(4),
|
||||
avgPerTask: settledCount > 0 ? (total / settledCount).toFixed(4) : '0',
|
||||
avgPaymentInterval: avgInterval ? `${avgInterval}s` : null,
|
||||
bySkill: Object.fromEntries(Object.entries(bySkill).map(([k, v]) => [k, { amount: v.toFixed(4), count: skillCounts[k] || 0 }])),
|
||||
byNetwork: Object.fromEntries(Object.entries(byNetwork).map(([k, v]) => [k, { amount: v.toFixed(4), gasless: k === NETWORKS.skale.caip2 }])),
|
||||
conversionRate: paymentLog.filter(p => p.type === 'payment-required').length > 0
|
||||
? ((settledCount / paymentLog.filter(p => p.type === 'payment-required').length) * 100).toFixed(1) + '%'
|
||||
: 'N/A',
|
||||
};
|
||||
}
|
||||
|
||||
function formatUptime(s) {
|
||||
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
|
||||
return d > 0 ? `${d}d ${h}h ${m}m` : h > 0 ? `${h}h ${m}m` : `${m}m`;
|
||||
@ -497,6 +618,11 @@ app.listen(PORT, () => {
|
||||
console.log(` Wallet: ${WALLET_ADDRESS}\n`);
|
||||
});
|
||||
|
||||
// Persist stats every 60s and on shutdown
|
||||
setInterval(saveStats, 60000);
|
||||
process.on('SIGTERM', () => { saveStats(); process.exit(0); });
|
||||
process.on('SIGINT', () => { saveStats(); process.exit(0); });
|
||||
|
||||
// === Dashboard HTML ===
|
||||
function getDashboardHtml() {
|
||||
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>OpSpawn A2A x402 Gateway</title>
|
||||
@ -509,13 +635,13 @@ function getDashboardHtml() {
|
||||
<div class="card"><h2>Agent Skills</h2><div class="sc"><span class="sn">Web Screenshot</span><span class="sp pd">$0.01</span><div class="sd">Capture any webpage as PNG. Send URL in message.</div></div><div class="sc"><span class="sn">Markdown to PDF</span><span class="sp pd">$0.005</span><div class="sd">Convert markdown to styled PDF document.</div></div><div class="sc"><span class="sn">Markdown to HTML</span><span class="sp fr">FREE</span><div class="sd">Convert markdown to styled HTML.</div></div></div>
|
||||
<div class="card"><h2>Endpoints</h2><ul class="el"><li><span>GET</span> /.well-known/agent-card.json</li><li><span>POST</span> / (message/send)</li><li><span>POST</span> / (tasks/get)</li><li><span>POST</span> / (tasks/cancel)</li><li><span>GET</span> /x402</li><li><span>GET</span> /api/info</li><li><span>GET</span> /api/payments</li><li><span>GET</span> /stats</li><li><span>GET</span> /health</li></ul></div>
|
||||
<div class="card"><h2>Payment Info (x402 V2)</h2><div class="sr"><span class="sl">Networks</span><span class="sv">Base (eip155:8453) + SKALE Europa (eip155:2046399126)</span></div><div class="sr"><span class="sl">Token</span><span class="sv">USDC</span></div><div class="sr"><span class="sl">Wallet</span><span class="sv" style="font-size:.75rem;word-break:break-all">${WALLET_ADDRESS}</span></div><div class="sr"><span class="sl">Facilitator</span><span class="sv">PayAI</span></div><div class="sr"><span class="sl">Protocol</span><span class="sv">x402 V2 + A2A v0.3</span></div><div class="sr"><span class="sl">SIWx</span><span class="sv" style="color:#66ffcc">Active (pay once, reuse)</span></div><div class="sr"><span class="sl">SIWx Sessions</span><span class="sv" id="ss">0</span></div></div>
|
||||
<div class="card"><h2>Live Stats</h2><div class="sr"><span class="sl">Payment Events</span><span class="sv" id="sp">0</span></div><div class="sr"><span class="sl">Tasks</span><span class="sv" id="st">0</span></div><div class="sr"><span class="sl">Uptime</span><span class="sv" id="su">0s</span></div><div class="sr"><span class="sl">Agent Card</span><span class="sv"><a href="/.well-known/agent-card.json" style="color:#4da6ff">View JSON</a></span></div><div id="pl" style="margin-top:1rem;max-height:200px;overflow-y:auto"></div></div>
|
||||
<div class="card"><h2>Live Stats</h2><div class="sr"><span class="sl">Payment Events</span><span class="sv" id="sp">0</span></div><div class="sr"><span class="sl">Tasks</span><span class="sv" id="st">0</span></div><div class="sr"><span class="sl">Revenue</span><span class="sv" id="sr-rev" style="color:#4dff88">$0.0000</span></div><div class="sr"><span class="sl">Conversion Rate</span><span class="sv" id="sr-conv">N/A</span></div><div class="sr"><span class="sl">Uptime</span><span class="sv" id="su">0s</span></div><div class="sr"><span class="sl">Agent Card</span><span class="sv"><a href="/.well-known/agent-card.json" style="color:#4da6ff">View JSON</a></span></div><h3 style="color:#888;font-size:.9rem;margin-top:1rem;margin-bottom:.5rem">Recent Activity</h3><div id="pl" style="max-height:200px;overflow-y:auto"></div></div>
|
||||
</div>
|
||||
<div class="ts"><h2>Try It: Send A2A Message</h2><p style="color:#888;margin-bottom:1rem;font-size:.9rem">Free <b>Markdown to HTML</b> executes immediately. Paid skills return payment requirements.</p><div class="tf"><input type="text" id="ti" placeholder="Enter markdown or URL" value="# Hello from A2A This is a **test**."><button id="tb" onclick="go()">Send A2A Message</button></div><div id="result"></div></div>
|
||||
</div>
|
||||
<footer>Built by <a href="https://opspawn.com">OpSpawn</a> for the SF Agentic Commerce x402 Hackathon | x402 V2 + A2A v0.3 + SIWx + SKALE Europa | <a href="/x402">Catalog</a> | <a href="/.well-known/agent-card.json">Agent Card</a> | <a href="/api/siwx">SIWx Sessions</a></footer>
|
||||
<script>
|
||||
async function rf(){try{const r=await fetch('/api/info'),d=await r.json();document.getElementById('sp').textContent=d.stats.payments;document.getElementById('st').textContent=d.stats.tasks;const ss=document.getElementById('ss');if(ss)ss.textContent=d.stats.siwxSessions||0;const s=Math.round(d.stats.uptime),h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sec=s%60;document.getElementById('su').textContent=h>0?h+'h '+m+'m':m>0?m+'m '+sec+'s':sec+'s'}catch(e){}try{const r=await fetch('/api/payments'),d=await r.json(),el=document.getElementById('pl');if(d.payments.length)el.innerHTML=d.payments.slice(-10).reverse().map(p=>'<div class="le"><span class="lt">'+(p.timestamp?.split('T')[1]?.split('.')[0]||'')+'</span> <span class="ly '+p.type+'">'+p.type+'</span> '+(p.skill||'')+'</div>').join('')}catch(e){}}rf();setInterval(rf,3000);
|
||||
async function rf(){try{const r=await fetch('/api/info'),d=await r.json();document.getElementById('sp').textContent=d.stats.payments;document.getElementById('st').textContent=d.stats.tasks;const ss=document.getElementById('ss');if(ss)ss.textContent=d.stats.siwxSessions||0;const s=Math.round(d.stats.uptime),h=Math.floor(s/3600),m=Math.floor((s%3600)/60),sec=s%60;document.getElementById('su').textContent=h>0?h+'h '+m+'m':m>0?m+'m '+sec+'s':sec+'s'}catch(e){}}async function rs(){try{const r=await fetch('/stats'),d=await r.json();const re=document.getElementById('sr-rev');if(re)re.textContent='$'+d.payments.revenue.total+' USDC';const cr=document.getElementById('sr-conv');if(cr)cr.textContent=d.payments.revenue.conversionRate||'N/A'}catch(e){}}async function rp(){try{const r=await fetch('/api/payments'),d=await r.json(),el=document.getElementById('pl');if(d.payments.length)el.innerHTML=d.payments.slice(-10).reverse().map(p=>'<div class="le"><span class="lt">'+(p.timestamp?.split('T')[1]?.split('.')[0]||'')+'</span> <span class="ly '+p.type+'">'+p.type+'</span> '+(p.skill||'')+'</div>').join('')}catch(e){}}rf();rs();rp();setInterval(()=>{rf();rs();rp()},3000);
|
||||
async function go(){const i=document.getElementById('ti').value,b=document.getElementById('tb'),r=document.getElementById('result');b.disabled=true;b.textContent='Sending...';r.style.display='block';r.textContent='Sending...';try{const body={jsonrpc:'2.0',id:crypto.randomUUID(),method:'message/send',params:{message:{messageId:crypto.randomUUID(),role:'user',parts:[{kind:'text',text:i}],kind:'message'},configuration:{blocking:true,acceptedOutputModes:['text/plain','text/html','application/json']}}};const resp=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const d=await resp.json();r.textContent=JSON.stringify(d,null,2);rf()}catch(e){r.textContent='Error: '+e.message}b.disabled=false;b.textContent='Send A2A Message'}
|
||||
</script></body></html>`;
|
||||
}
|
||||
@ -526,6 +652,13 @@ function getDemoHtml() {
|
||||
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>A2A x402 Gateway — Live Demo</title>
|
||||
<meta name="description" content="Watch AI agents discover, negotiate, and pay each other using A2A protocol + x402 V2 micropayments. Live interactive demo.">
|
||||
<meta property="og:title" content="A2A x402 Gateway — Pay-Per-Request Agent Services">
|
||||
<meta property="og:description" content="AI agents discover, negotiate, and pay each other for services using A2A + x402 V2 micropayments on Base and SKALE Europa.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="${publicUrl}/demo">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="A2A x402 Gateway — Agent Micropayments">
|
||||
<meta name="twitter:description" content="Live demo: AI agents paying agents with USDC micropayments via A2A protocol + x402 V2.">
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0a0a;color:#e0e0e0;min-height:100vh;overflow-x:hidden}
|
||||
|
||||
525
stats.json
Normal file
525
stats.json
Normal file
@ -0,0 +1,525 @@
|
||||
{
|
||||
"paymentLog": [
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "bd8ceb44-c41d-496f-87b5-434a85071cf0",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T12:57:12.554Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "9bf61c42-52cf-4e7e-b2e6-9870dc9f163e",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T12:57:12.558Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "466179de-ce73-4deb-af7f-cd2f755ab25b",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T12:57:12.561Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "22579709-61ac-4047-a23a-20384c974b43",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:04:54.758Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "fb55e12e-c7f3-41dd-b734-71d58c148187",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:04:54.763Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-received",
|
||||
"taskId": "165b7249-a214-492f-b6bb-df5e83f10d8c",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T13:04:54.768Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-settled",
|
||||
"taskId": "165b7249-a214-492f-b6bb-df5e83f10d8c",
|
||||
"skill": "screenshot",
|
||||
"txHash": "0x694a9d5e5fa54cb6a40ad6c259ca79ab",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T13:05:01.092Z"
|
||||
},
|
||||
{
|
||||
"type": "siwx-access",
|
||||
"taskId": "084f84d9-748e-4229-9e73-43e477a7f63c",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:05:01.102Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "85192e57-c401-46fa-9456-32d3b614dcbf",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:05:06.907Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "dc57798c-8c17-4775-a7df-8fe9f9a15d5b",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:05:06.958Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "0d5cad6f-7514-484f-9a00-fe2acaec4cef",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:05:07.001Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "f9c7c1e2-134d-477b-9812-3251859c9a8b",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:05:32.964Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "c052a79e-4f65-4a83-ad0b-a022b07b2d9c",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:05:32.968Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "c0f84b5b-5030-4307-9c16-572c7804ca76",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:05:32.971Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "4f1c7ee4-8643-418e-ab2f-8b1b458377b3",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:37:48.705Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "1234d468-0573-4879-8b8e-b699d969ddbd",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:37:48.709Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-received",
|
||||
"taskId": "4a0c0055-774c-44b2-9ad2-54ed6919a4b9",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T13:37:48.713Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-settled",
|
||||
"taskId": "4a0c0055-774c-44b2-9ad2-54ed6919a4b9",
|
||||
"skill": "screenshot",
|
||||
"txHash": "0x856623ec996342ab94fd006ef0c77a57",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T13:37:53.232Z"
|
||||
},
|
||||
{
|
||||
"type": "siwx-access",
|
||||
"taskId": "819aa9f0-666c-4b26-9282-3e97197d4d39",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:37:53.243Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "6c8aafe6-6dfc-419e-a019-14525a496e4c",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:37:57.223Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "9ee91e90-8aec-4442-ba49-8e15b4449992",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:37:57.251Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "9f0c64b0-871e-492e-ad70-ec49f7e8323a",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:37:57.277Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "e26ec3fd-ff14-497e-b708-ea6ae6157acd",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:38:19.748Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "381ad315-fad5-48da-a73a-4d368ce46a0a",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:38:19.752Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "83c849f0-f577-4c35-b291-c1e0f91243fb",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T13:38:19.755Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "0ddfc02b-eb4d-4133-8ce8-f58c7c0918bf",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:09:44.236Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "03155b4b-55e0-4450-9d57-fdd013137f5c",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:09:44.240Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-received",
|
||||
"taskId": "9f711e8f-4800-4269-9e25-7bb1388fcddc",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T14:09:44.244Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-settled",
|
||||
"taskId": "9f711e8f-4800-4269-9e25-7bb1388fcddc",
|
||||
"skill": "screenshot",
|
||||
"txHash": "0x56f65293abb74dbdb695e52436c18b94",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T14:09:48.605Z"
|
||||
},
|
||||
{
|
||||
"type": "siwx-access",
|
||||
"taskId": "b51ef6d9-cd2d-4709-927d-54a6e9ecaab2",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:09:48.612Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "b38ab108-b19e-46db-be22-4ccde7715848",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:09:52.847Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "0e76aaae-0745-494e-b2ba-16a77cbc790c",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:09:52.864Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "b3443328-e91f-434f-9eda-fd7ed6e9ca0d",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:09:52.877Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "7e5a9228-69ce-427c-84dd-7d4a3830fc0f",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:10:14.306Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "2dbff088-196d-4910-b4ef-89a8d6ff8740",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:10:14.309Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "495521cf-da3f-4b29-8c1c-0006ed550370",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:10:14.312Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "3827e99b-cd15-43e0-a4c8-edeaf45c5db2",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:01.208Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "0cffd2d7-4a29-4450-ab2e-b158f607a618",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:01.217Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-received",
|
||||
"taskId": "af9983be-f09e-453d-a162-9d3bbda4bc66",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T14:45:01.222Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-settled",
|
||||
"taskId": "af9983be-f09e-453d-a162-9d3bbda4bc66",
|
||||
"skill": "screenshot",
|
||||
"txHash": "0xa14ee92557e9459f82d77ea028f61881",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T14:45:06.025Z"
|
||||
},
|
||||
{
|
||||
"type": "siwx-access",
|
||||
"taskId": "c507c4e5-fbd6-41ae-a3bc-7014dc428dfc",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:06.040Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "a622f7ea-1559-44de-a1cc-558a60cf1cf4",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:10.575Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "319cd090-f209-4b79-ade4-d7be900a9d6d",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:10.600Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "0fc8faaf-4b0a-4f5e-8054-d0b39753c633",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:10.624Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "dfcc906f-4d26-47d0-8d60-5490475b3437",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:50.627Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "e394365f-7813-4fae-bfe0-0cc95a4261eb",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:50.630Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-received",
|
||||
"taskId": "061f022d-40a4-4bff-80a7-15415e46bd74",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T14:45:50.633Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-settled",
|
||||
"taskId": "061f022d-40a4-4bff-80a7-15415e46bd74",
|
||||
"skill": "screenshot",
|
||||
"txHash": "0x63327f2eca4142e6b4e58039587e276d",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T14:45:54.944Z"
|
||||
},
|
||||
{
|
||||
"type": "siwx-access",
|
||||
"taskId": "ae2aaf4a-b7ff-4db1-958c-c5626fb109df",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:54.954Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "b0dd33ed-802d-404c-b3d3-928917f28640",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:58.826Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "d430636b-0366-45fa-8533-14685913e443",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:58.851Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "f7d11bfb-de6a-4873-af23-28943ea20f05",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T14:45:58.869Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "11e21f06-c12e-4ff1-9032-6cf1c33be800",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:17:52.488Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "145e5848-3c1b-4b6b-bf51-3f35685e21db",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:17:52.492Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-received",
|
||||
"taskId": "1b90577d-990b-4ccb-8798-b740b73e36de",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T15:17:52.497Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-settled",
|
||||
"taskId": "1b90577d-990b-4ccb-8798-b740b73e36de",
|
||||
"skill": "screenshot",
|
||||
"txHash": "0x69bfad5f47c14b2ab9ece8d043f06e84",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": "eip155:8453",
|
||||
"timestamp": "2026-02-07T15:17:56.148Z"
|
||||
},
|
||||
{
|
||||
"type": "siwx-access",
|
||||
"taskId": "94c54b64-f926-47e6-ad44-7a2f6ef60ff5",
|
||||
"skill": "screenshot",
|
||||
"wallet": "0xTestWallet123",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:17:56.156Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "40f3e312-095c-4c42-9731-88b9b523155e",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:18:00.339Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "a9e8ce9d-4f83-4eec-8ff3-a4a1b9b4aef3",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:18:00.372Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "53b6784b-0506-4234-8ed8-93f5e0afdf56",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:18:00.400Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "6fc187e9-14fb-4e3a-b5dc-ecadf4d99db8",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:18:36.447Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "2022539a-903b-47a3-b399-466450c272b8",
|
||||
"skill": "screenshot",
|
||||
"amount": "$0.01",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:18:36.450Z"
|
||||
},
|
||||
{
|
||||
"type": "payment-required",
|
||||
"taskId": "65889c04-959b-4fbd-b6ea-caf5120f9087",
|
||||
"skill": "markdown-to-pdf",
|
||||
"amount": "$0.005",
|
||||
"network": null,
|
||||
"timestamp": "2026-02-07T15:18:36.452Z"
|
||||
}
|
||||
],
|
||||
"siwxSessions": {
|
||||
"0xtestwallet123": {
|
||||
"skills": [
|
||||
"screenshot"
|
||||
],
|
||||
"lastPayment": "2026-02-07T15:17:52.497Z"
|
||||
}
|
||||
},
|
||||
"totalTasks": 78,
|
||||
"startedAt": "2026-02-07T12:55:59.752Z",
|
||||
"savedAt": "2026-02-07T15:24:50.122Z"
|
||||
}
|
||||
51
test.mjs
51
test.mjs
@ -26,7 +26,7 @@ await test('GET /.well-known/agent-card.json returns valid V2 agent card', async
|
||||
assert(r.status === 200);
|
||||
const d = await r.json();
|
||||
assert(d.name === 'OpSpawn Screenshot Agent');
|
||||
assert(d.version === '2.0.0', `Version: ${d.version}`);
|
||||
assert(d.version === '2.1.0', `Version: ${d.version}`);
|
||||
assert(d.skills.length === 3);
|
||||
assert(d.skills[0].id === 'screenshot');
|
||||
assert(d.protocolVersion === '0.3.0');
|
||||
@ -43,7 +43,7 @@ await test('GET /.well-known/agent-card.json returns valid V2 agent card', async
|
||||
await test('GET /x402 returns V2 service catalog', async () => {
|
||||
const r = await fetch(`${BASE}/x402`);
|
||||
const d = await r.json();
|
||||
assert(d.version === '2.0.0', `Version: ${d.version}`);
|
||||
assert(d.version === '2.1.0', `Version: ${d.version}`);
|
||||
assert(d.protocols.a2a.version === '0.3.0');
|
||||
assert(d.protocols.x402.version === '2.0', `x402 version: ${d.protocols.x402.version}`);
|
||||
assert(d.protocols.x402.networks.length >= 2, 'Has multiple networks');
|
||||
@ -343,5 +343,52 @@ await test('GET /api/payments reflects activity', async () => {
|
||||
assert(d.payments.some(p => p.type === 'payment-required'), 'Has payment-required entry');
|
||||
});
|
||||
|
||||
await test('SKALE Europa: correct chain ID in agent card', async () => {
|
||||
const r = await fetch(`${BASE}/.well-known/agent-card.json`);
|
||||
const d = await r.json();
|
||||
const payExt = d.extensions.find(e => e.uri === 'urn:x402:payment:v2');
|
||||
const skaleNet = payExt.config.networks.find(n => n.name === 'SKALE Europa');
|
||||
assert(skaleNet, 'SKALE Europa network present');
|
||||
assert(skaleNet.network === 'eip155:2046399126', `SKALE Europa CAIP-2: ${skaleNet.network}`);
|
||||
assert(skaleNet.gasless === true, 'SKALE is gasless');
|
||||
assert(skaleNet.tokenAddress === '0x5F795bb52dAC3085f578f4877D450e2929D2F13d', `SKALE USDC: ${skaleNet.tokenAddress}`);
|
||||
});
|
||||
|
||||
await test('SKALE Europa: correct USDC in payment requirements', async () => {
|
||||
const r = await fetch(BASE, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0', id: 'test-skale-pay',
|
||||
method: 'message/send',
|
||||
params: {
|
||||
message: {
|
||||
messageId: 'msg-skale', role: 'user', kind: 'message',
|
||||
parts: [{ kind: 'text', text: 'Take a screenshot of https://example.com' }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
const d = await r.json();
|
||||
const accepts = d.result.status.message.parts.find(p => p.kind === 'data')?.data['x402.accepts'];
|
||||
const skaleAccept = accepts.find(a => a.network === 'eip155:2046399126');
|
||||
assert(skaleAccept, 'SKALE Europa in payment accepts');
|
||||
assert(skaleAccept.asset === '0x5F795bb52dAC3085f578f4877D450e2929D2F13d', `SKALE USDC asset: ${skaleAccept.asset}`);
|
||||
assert(skaleAccept.gasless === true, 'Marked gasless');
|
||||
// Base should use different USDC address
|
||||
const baseAccept = accepts.find(a => a.network === 'eip155:8453');
|
||||
assert(baseAccept.asset !== skaleAccept.asset, 'Base and SKALE use different USDC addresses');
|
||||
});
|
||||
|
||||
await test('SKALE Europa: x402 catalog shows correct chain details', async () => {
|
||||
const r = await fetch(`${BASE}/x402`);
|
||||
const d = await r.json();
|
||||
const skaleNet = d.protocols.x402.networks.find(n => n.name === 'SKALE Europa');
|
||||
assert(skaleNet, 'SKALE Europa in catalog');
|
||||
assert(skaleNet.chainId === 2046399126, `Chain ID: ${skaleNet.chainId}`);
|
||||
assert(skaleNet.gasless === true, 'Gasless flag');
|
||||
assert(skaleNet.network === 'eip155:2046399126', 'CAIP-2 format');
|
||||
});
|
||||
|
||||
console.log(`\nResults: ${passed} passed, ${failed} failed, ${passed + failed} total\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user