/** * A2A x402 Gateway v2 - Agent Network with Micropayments * * An A2A-compliant agent server that exposes screenshot/document services * with x402 V2 cryptocurrency micropayments on Base + SKALE networks. * * 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) * - Express HTTP server with web dashboard * * Built by OpSpawn for the SF Agentic Commerce x402 Hackathon */ import express from 'express'; import { v4 as uuidv4 } from 'uuid'; // === Configuration === const PORT = parseInt(process.env.PORT || '4002', 10); const SNAPAPI_URL = process.env.SNAPAPI_URL || 'http://localhost:3001'; const SNAPAPI_KEY = process.env.SNAPAPI_API_KEY || 'demo-key-001'; const WALLET_ADDRESS = '0x7483a9F237cf8043704D6b17DA31c12BfFF860DD'; const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; const FACILITATOR_URL = 'https://facilitator.payai.network'; const PUBLIC_URL = process.env.PUBLIC_URL || `http://localhost:${PORT}`; // x402 V2: CAIP-2 network identifiers const NETWORKS = { base: { caip2: 'eip155:8453', name: 'Base', chainId: 8453, usdc: BASE_USDC }, skale: { caip2: 'eip155:324705682', name: 'SKALE', chainId: 324705682, usdc: BASE_USDC, gasless: true }, }; const DEFAULT_NETWORK = NETWORKS.base; // === State === const tasks = new Map(); const paymentLog = []; // === SIWx session store (in-memory) === const siwxSessions = new Map(); // wallet address -> { paidSkills: Set, lastPayment: timestamp } function recordSiwxPayment(walletAddress, skill) { const normalized = walletAddress.toLowerCase(); if (!siwxSessions.has(normalized)) { siwxSessions.set(normalized, { paidSkills: new Set(), lastPayment: null }); } const session = siwxSessions.get(normalized); session.paidSkills.add(skill); session.lastPayment = new Date().toISOString(); } function hasSiwxAccess(walletAddress, skill) { const normalized = walletAddress?.toLowerCase(); if (!normalized) return false; const session = siwxSessions.get(normalized); return session?.paidSkills.has(skill) || false; } // === Agent Card (A2A v0.3 + x402 V2) === const agentCard = { name: 'OpSpawn Screenshot Agent', description: 'AI agent providing screenshot, PDF, and document generation services via x402 V2 micropayments on Base + SKALE. 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', protocolVersion: '0.3.0', capabilities: { streaming: false, pushNotifications: false, stateTransitionHistory: true, }, defaultInputModes: ['text/plain', 'application/json'], defaultOutputModes: ['image/png', 'application/pdf', 'text/html', 'text/plain'], skills: [ { id: 'screenshot', name: 'Web Screenshot', description: 'Capture a screenshot of any URL. Returns PNG image. Price: $0.01 USDC on Base.', tags: ['screenshot', 'web', 'capture', 'image', 'x402', 'x402-v2'], examples: ['Take a screenshot of https://example.com'], inputModes: ['text/plain'], outputModes: ['image/png', 'image/jpeg'], }, { id: 'markdown-to-pdf', name: 'Markdown to PDF', description: 'Convert markdown text to a styled PDF document. Price: $0.005 USDC on Base.', tags: ['markdown', 'pdf', 'document', 'conversion', 'x402', 'x402-v2'], examples: ['Convert to PDF: # Hello World'], inputModes: ['text/plain'], outputModes: ['application/pdf'], }, { id: 'markdown-to-html', name: 'Markdown to HTML', description: 'Convert markdown to styled HTML. Free endpoint — no payment required.', tags: ['markdown', 'html', 'conversion', 'free'], examples: ['Convert to HTML: # Hello World'], inputModes: ['text/plain'], outputModes: ['text/html'], }, ], extensions: [ { uri: 'urn:x402:payment:v2', config: { version: '2.0', networks: [ { network: NETWORKS.base.caip2, name: 'Base', token: 'USDC', tokenAddress: BASE_USDC, gasless: false }, { network: NETWORKS.skale.caip2, name: 'SKALE', token: 'USDC', tokenAddress: BASE_USDC, gasless: true }, ], wallet: WALLET_ADDRESS, facilitator: FACILITATOR_URL, features: ['siwx', 'payment-identifier', 'bazaar-discovery'], }, }, ], }; // === Task helpers === function createTask(id, contextId, state, message) { const task = { id, contextId, status: { state, timestamp: new Date().toISOString(), ...(message && { message }) }, history: [], artifacts: [], metadata: {}, }; tasks.set(id, task); return task; } function updateTask(taskId, state, message, metadata) { const task = tasks.get(taskId); if (!task) return null; task.status = { state, timestamp: new Date().toISOString(), ...(message && { message }) }; if (metadata) Object.assign(task.metadata, metadata); return task; } // === Request parsing === function parseRequest(text) { const lower = text.toLowerCase(); if (lower.includes('pdf') && !lower.startsWith('http')) { return { skill: 'markdown-to-pdf', markdown: text.replace(/^.*?(?:pdf|convert).*?:\s*/i, '').trim() || text }; } if (lower.includes('html') && !lower.startsWith('http')) { return { skill: 'markdown-to-html', markdown: text.replace(/^.*?(?:html|convert).*?:\s*/i, '').trim() || text }; } const urlMatch = text.match(/https?:\/\/[^\s]+/); if (urlMatch) return { skill: 'screenshot', url: urlMatch[0] }; return { skill: 'markdown-to-html', markdown: text }; } // === Service handlers === 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 } }); if (!resp.ok) throw new Error(`SnapAPI error: ${resp.status}`); const buffer = Buffer.from(await resp.arrayBuffer()); return { parts: [ { kind: 'text', text: `Screenshot captured for ${url} (${Math.round(buffer.length/1024)}KB)` }, { kind: 'file', name: 'screenshot.png', mimeType: 'image/png', data: buffer.toString('base64') }, ], }; } async function handleMarkdownToPdf(markdown) { const resp = await fetch(`${SNAPAPI_URL}/api/md2pdf`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': SNAPAPI_KEY }, body: JSON.stringify({ markdown }), }); if (!resp.ok) throw new Error(`SnapAPI error: ${resp.status}`); const buffer = Buffer.from(await resp.arrayBuffer()); return { parts: [ { kind: 'text', text: `PDF generated (${Math.round(buffer.length/1024)}KB)` }, { kind: 'file', name: 'document.pdf', mimeType: 'application/pdf', data: buffer.toString('base64') }, ], }; } async function handleMarkdownToHtml(markdown) { const resp = await fetch(`${SNAPAPI_URL}/api/md2html`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ markdown, theme: 'light' }), }); if (!resp.ok) throw new Error(`SnapAPI error: ${resp.status}`); const html = await resp.text(); return { parts: [ { kind: 'text', text: `Converted markdown to HTML (${html.length} bytes)` }, { kind: 'data', data: { html, format: 'text/html', bytes: html.length } }, ], }; } // === x402 V2 Payment Requirements === function createPaymentRequired(skill) { const pricing = { screenshot: { price: '$0.01', amount: '10000', description: 'Screenshot - $0.01 USDC' }, 'markdown-to-pdf': { price: '$0.005', amount: '5000', description: 'Markdown to PDF - $0.005 USDC' }, }; const p = pricing[skill]; if (!p) return null; // V2 format: accepts array with CAIP-2 network IDs return { version: '2.0', accepts: [ { scheme: 'exact', network: NETWORKS.base.caip2, price: p.price, payTo: WALLET_ADDRESS, asset: BASE_USDC, maxAmountRequired: p.amount, // backward compat with V1 clients }, { scheme: 'exact', network: NETWORKS.skale.caip2, price: p.price, payTo: WALLET_ADDRESS, asset: BASE_USDC, gasless: true, }, ], resource: `/${skill}`, description: p.description, facilitator: FACILITATOR_URL, extensions: { 'sign-in-with-x': { supported: true, statement: `Sign in to access ${skill} without repaying`, chains: [NETWORKS.base.caip2, NETWORKS.skale.caip2], }, 'payment-identifier': { supported: true }, }, }; } // === JSON-RPC handler === async function handleJsonRpc(req, res) { const { jsonrpc, id, method, params } = req.body; if (jsonrpc !== '2.0') return res.json({ jsonrpc: '2.0', id, error: { code: -32600, message: 'Invalid Request' } }); switch (method) { case 'message/send': case 'tasks/send': return handleMessageSend(id, params, res); case 'tasks/get': return handleTasksGet(id, params, res); case 'tasks/cancel': return handleTasksCancel(id, params, res); default: return res.json({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }); } } async function handleMessageSend(rpcId, params, res) { const { message } = params || {}; if (!message?.parts?.length) return res.json({ jsonrpc: '2.0', id: rpcId, error: { code: -32602, message: 'message.parts required' } }); const textPart = message.parts.find(p => p.kind === 'text' || p.type === 'text'); if (!textPart) return res.json({ jsonrpc: '2.0', id: rpcId, error: { code: -32602, message: 'text part required' } }); const taskId = uuidv4(); const contextId = message.contextId || uuidv4(); const request = parseRequest(textPart.text); // Check for V2 PAYMENT-SIGNATURE header (direct x402 V2 payment) const paymentSignature = params?.metadata?.['x402.payment.signature'] || message.metadata?.['x402.payment.payload']; if (paymentSignature) return handlePaidExecution(rpcId, taskId, contextId, request, paymentSignature, message, res); // Check for SIWx session-based access (V2: wallet already paid before) 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() }); return handleFreeExecution(rpcId, taskId, contextId, request, message, res); } // Paid skill? Return V2 payment requirements const payReq = createPaymentRequired(request.skill); if (payReq) { const task = createTask(taskId, contextId, 'input-required', { kind: 'message', role: 'agent', messageId: uuidv4(), parts: [ { kind: 'text', text: `Payment required: ${payReq.description}` }, { kind: 'data', data: { 'x402.payment.required': true, 'x402.version': '2.0', 'x402.accepts': payReq.accepts, 'x402.extensions': payReq.extensions, skill: request.skill, }}, ], taskId, contextId, }); task.metadata['x402.accepts'] = payReq.accepts; 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() }); return res.json({ jsonrpc: '2.0', id: rpcId, result: task }); } // Free: execute immediately return handleFreeExecution(rpcId, taskId, contextId, request, message, res); } async function handleFreeExecution(rpcId, taskId, contextId, request, message, res) { const task = createTask(taskId, contextId, 'working'); task.history.push(message); try { let result; if (request.skill === 'screenshot' && request.url) result = await handleScreenshot(request.url); else if (request.skill === 'markdown-to-pdf') result = await handleMarkdownToPdf(request.markdown || '# Document'); else result = await handleMarkdownToHtml(request.markdown || request.url || '# Hello'); updateTask(taskId, 'completed', { kind: 'message', role: 'agent', messageId: uuidv4(), parts: result.parts, taskId, contextId, }); return res.json({ jsonrpc: '2.0', id: rpcId, result: tasks.get(taskId) }); } catch (err) { updateTask(taskId, 'failed', { kind: 'message', role: 'agent', messageId: uuidv4(), parts: [{ kind: 'text', text: `Error: ${err.message}` }], taskId, contextId, }); return res.json({ jsonrpc: '2.0', id: rpcId, result: tasks.get(taskId) }); } } 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() }); // Record SIWx session so the payer can re-access without paying again if (payerWallet !== 'unknown') { recordSiwxPayment(payerWallet, request.skill); console.log(`[siwx] Session recorded for ${payerWallet} -> ${request.skill}`); } const task = createTask(taskId, contextId, 'working'); task.history.push(message); try { let result; if (request.skill === 'screenshot' && request.url) result = await handleScreenshot(request.url); else if (request.skill === 'markdown-to-pdf') result = await handleMarkdownToPdf(request.markdown || '# Document'); 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() }); updateTask(taskId, 'completed', { kind: 'message', role: 'agent', messageId: uuidv4(), parts: result.parts, taskId, contextId, }, { 'x402.payment.settled': true, 'x402.txHash': txHash, 'x402.version': '2.0', 'x402.siwx.active': payerWallet !== 'unknown', }); return res.json({ jsonrpc: '2.0', id: rpcId, result: tasks.get(taskId) }); } catch (err) { updateTask(taskId, 'failed', { kind: 'message', role: 'agent', messageId: uuidv4(), parts: [{ kind: 'text', text: `Error: ${err.message}` }], taskId, contextId, }); return res.json({ jsonrpc: '2.0', id: rpcId, result: tasks.get(taskId) }); } } function handleTasksGet(rpcId, params, res) { const task = tasks.get(params?.id); if (!task) return res.json({ jsonrpc: '2.0', id: rpcId, error: { code: -32001, message: 'Task not found' } }); return res.json({ jsonrpc: '2.0', id: rpcId, result: task }); } function handleTasksCancel(rpcId, params, res) { const task = tasks.get(params?.id); if (!task) return res.json({ jsonrpc: '2.0', id: rpcId, error: { code: -32001, message: 'Task not found' } }); updateTask(params.id, 'canceled'); return res.json({ jsonrpc: '2.0', id: rpcId, result: task }); } // === Express App === const app = express(); app.use(express.json({ limit: '10mb' })); app.use('/public', express.static(new URL('./public', import.meta.url).pathname)); app.get('/.well-known/agent-card.json', (req, res) => res.json(agentCard)); app.get('/.well-known/agent.json', (req, res) => res.json(agentCard)); app.post('/', handleJsonRpc); app.post('/a2a', handleJsonRpc); app.get('/', (req, res) => res.redirect('/dashboard')); app.get('/dashboard', (req, res) => res.type('html').send(getDashboardHtml())); app.get('/api/info', (req, res) => res.json({ agent: agentCard, payments: { version: '2.0', networks: Object.values(NETWORKS).map(n => ({ network: n.caip2, name: n.name, gasless: n.gasless || false })), token: 'USDC', wallet: WALLET_ADDRESS, facilitator: FACILITATOR_URL, features: ['siwx', 'payment-identifier', 'bazaar-discovery'], services: { screenshot: '$0.01', 'markdown-to-pdf': '$0.005', 'markdown-to-html': 'free' }, }, stats: { payments: paymentLog.length, tasks: tasks.size, uptime: process.uptime(), siwxSessions: siwxSessions.size, paymentsByType: { required: paymentLog.filter(p => p.type === 'payment-required').length, received: paymentLog.filter(p => p.type === 'payment-received').length, settled: paymentLog.filter(p => p.type === 'payment-settled').length, siwxAccess: paymentLog.filter(p => p.type === 'siwx-access').length, }, }, })); app.get('/api/payments', (req, res) => res.json({ payments: paymentLog.slice(-50), total: paymentLog.length })); app.get('/api/siwx', (req, res) => { const sessions = []; for (const [wallet, data] of siwxSessions.entries()) { sessions.push({ wallet, skills: [...data.paidSkills], lastPayment: data.lastPayment }); } res.json({ sessions, total: sessions.length }); }); app.get('/x402', (req, res) => res.json({ service: 'OpSpawn A2A x402 Gateway', version: '2.0.0', description: 'A2A-compliant agent with x402 V2 micropayment services on Base + SKALE', provider: { name: 'OpSpawn', url: 'https://opspawn.com' }, protocols: { a2a: { version: '0.3.0', agentCard: '/.well-known/agent-card.json', sendMessage: '/' }, x402: { version: '2.0', networks: Object.values(NETWORKS).map(n => ({ network: n.caip2, name: n.name, chainId: n.chainId, token: 'USDC', tokenAddress: n.usdc, gasless: n.gasless || false, })), facilitator: FACILITATOR_URL, wallet: WALLET_ADDRESS, features: { siwx: 'Sign-In-With-X session auth — pay once, access again without repaying', 'payment-identifier': 'Idempotent payments — retries do not double-charge', 'bazaar-discovery': 'Machine-readable API schemas in payment requirements', }, }, }, endpoints: [ { skill: 'screenshot', price: '$0.01', description: 'Capture webpage as PNG', input: 'URL in text', output: 'image/png' }, { skill: 'markdown-to-pdf', price: '$0.005', description: 'Convert markdown to PDF', input: 'Markdown text', output: 'application/pdf' }, { skill: 'markdown-to-html', price: 'free', description: 'Convert markdown to HTML', input: 'Markdown text', output: 'text/html' }, ], })); app.get('/demo', (req, res) => res.type('html').send(getDemoHtml())); app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime(), timestamp: new Date().toISOString() })); // Standalone x402 HTTP 402 endpoint for judges testing standard x402 flow app.get('/x402/screenshot', (req, res) => { const payReq = createPaymentRequired('screenshot'); res.status(402).json(payReq); }); app.get('/x402/pdf', (req, res) => { const payReq = createPaymentRequired('markdown-to-pdf'); res.status(402).json(payReq); }); app.get('/favicon.ico', (req, res) => res.status(204).end()); app.listen(PORT, () => { console.log(`\n A2A x402 Gateway on http://localhost:${PORT}`); console.log(` Agent Card: /.well-known/agent-card.json`); console.log(` Dashboard: /dashboard`); console.log(` Services: screenshot($0.01), md-to-pdf($0.005), md-to-html(free)`); console.log(` Wallet: ${WALLET_ADDRESS}\n`); }); // === Dashboard HTML === function getDashboardHtml() { return `OpSpawn A2A x402 Gateway

OpSpawn A2A x402 Gateway

Pay-per-request AI agent services via A2A protocol + x402 V2 micropayments

A2A v0.3x402 V2Base USDCSKALESIWx

Payment Flow

Agent ClientA2A Gateway402: Pay USDCService Result

Agent sends A2A message → Gateway returns payment requirements → Agent signs USDC → Gateway delivers result

Agent Skills

Web Screenshot$0.01
Capture any webpage as PNG. Send URL in message.
Markdown to PDF$0.005
Convert markdown to styled PDF document.
Markdown to HTMLFREE
Convert markdown to styled HTML.

Endpoints

  • GET /.well-known/agent-card.json
  • POST / (message/send)
  • POST / (tasks/get)
  • POST / (tasks/cancel)
  • GET /x402
  • GET /api/info
  • GET /api/payments
  • GET /health

Payment Info (x402 V2)

NetworksBase (eip155:8453) + SKALE
TokenUSDC
Wallet${WALLET_ADDRESS}
FacilitatorPayAI
Protocolx402 V2 + A2A v0.3
SIWxActive (pay once, reuse)
SIWx Sessions0

Live Stats

Payment Events0
Tasks0
Uptime0s
Agent CardView JSON

Try It: Send A2A Message

Free Markdown to HTML executes immediately. Paid skills return payment requirements.

`; } // === Demo Page: Human-Facing Hackathon Demo === function getDemoHtml() { const publicUrl = PUBLIC_URL.replace(/\/$/, ''); return ` A2A x402 Gateway — Live Demo

A2A x402 Gateway

AI agents discover, negotiate, and pay each other for services — live, in real time, for fractions of a cent.

The first A2A agent with native x402 V2 micropayments
LIVE A2A v0.3 x402 V2 Base USDC SKALE Gasless SIWx Sessions
0
Tasks
0
Payments
0
SIWx Sessions
0s
Uptime

Watch the Demo

Architecture

AI Agent
Any A2A client
A2A Gateway
JSON-RPC v0.3
x402 Payment
USDC on Base
Service Result
PNG / PDF / HTML

A2A Protocol v0.3

Google's Agent-to-Agent standard. Discovery via /.well-known/agent-card.json, communication via JSON-RPC message/send.

x402 V2 Payments

Coinbase's HTTP payment protocol. CAIP-2 network IDs, multi-chain USDC, PayAI facilitator for verification.

SIWx Sessions

Sign-In-With-X (CAIP-122). Pay once for a skill, reuse forever. No API keys, no subscriptions.

Multi-Chain

Base eip155:8453 ($0.01 per screenshot) or SKALE eip155:324705682 (gasless).

Interactive Demo: Markdown to HTML FREE

Free skill — no payment needed. Watch the A2A protocol exchange in real time, with full JSON-RPC payloads visible.

1

Discover agent via standard A2A endpoint

GET /.well-known/agent-card.json
2

Send A2A message/send request

POST / — JSON-RPC 2.0 with markdown content
3

Gateway returns HTML result

Task state: completed — no payment needed for free skills

Rendered Output

Interactive Demo: Screenshot a Website $0.01 USDC

Paid skill — watch the full x402 payment flow: request → 402 payment required → sign USDC → result delivered.

1

Request screenshot of example.com

POST / — message/send with URL in natural language
2

Gateway returns x402 payment requirements

Task state: input-required — x402 V2 accepts Base USDC or SKALE gasless
3

Agent signs x402 USDC payment

Client creates payment authorization for $0.01 USDC on Base
4

Screenshot delivered + SIWx session created

Payment settled, wallet gets session access for future requests

Screenshot Result

Try It Yourself

This is a live API. Copy these commands and run them against the real endpoint.

1. Discover the agent

curl -s ${publicUrl}/.well-known/agent-card.json | jq '.'

2. Send a free A2A message (markdown to HTML)

curl -s -X POST ${publicUrl}/ \\ -H "Content-Type: application/json" \\ -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"messageId":"demo-1","role":"user","parts":[{"kind":"text","text":"Convert to HTML: # Hello World"}],"kind":"message"}}}' | jq '.'

3. Request a paid screenshot (returns payment requirements)

curl -s -X POST ${publicUrl}/ \\ -H "Content-Type: application/json" \\ -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"messageId":"demo-2","role":"user","parts":[{"kind":"text","text":"Take a screenshot of https://example.com"}],"kind":"message"}}}' | jq '.result.status'

4. View the x402 service catalog

curl -s ${publicUrl}/x402 | jq '.'
`; }