Add demo video and static file serving for hackathon submission
- Record 67s demo video showing A2A + x402 V2 payment flow - Add /public static serving for video hosting - Video accessible at /public/demo-video.mp4 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
939251912b
commit
bcf94fc19e
BIN
public/demo-video.mp4
Normal file
BIN
public/demo-video.mp4
Normal file
Binary file not shown.
255
server.mjs
255
server.mjs
@ -23,6 +23,7 @@ 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 = {
|
||||
@ -59,7 +60,7 @@ function hasSiwxAccess(walletAddress, skill) {
|
||||
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: `http://localhost:${PORT}/`,
|
||||
url: `${PUBLIC_URL}/`,
|
||||
provider: { organization: 'OpSpawn', url: 'https://opspawn.com' },
|
||||
version: '2.0.0',
|
||||
protocolVersion: '0.3.0',
|
||||
@ -384,6 +385,7 @@ function handleTasksCancel(rpcId, params, res) {
|
||||
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.post('/', handleJsonRpc);
|
||||
app.get('/dashboard', (req, res) => res.type('html').send(getDashboardHtml()));
|
||||
@ -441,6 +443,7 @@ app.get('/x402', (req, res) => res.json({
|
||||
{ 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() }));
|
||||
app.get('/favicon.ico', (req, res) => res.status(204).end());
|
||||
|
||||
@ -474,3 +477,253 @@ async function rf(){try{const r=await fetch('/api/info'),d=await r.json();docume
|
||||
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>`;
|
||||
}
|
||||
|
||||
// === Demo Page: Human-Facing Hackathon Demo ===
|
||||
function getDemoHtml() {
|
||||
return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>OpSpawn Demo — Agent Commerce in Action</title>
|
||||
<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}
|
||||
.hero{background:linear-gradient(135deg,#0a1628,#1a0a2e,#0a2e1a);padding:3rem 2rem;text-align:center;border-bottom:2px solid #00d4ff}
|
||||
.hero h1{font-size:2.5rem;background:linear-gradient(90deg,#00d4ff,#4dff88,#ffcc00);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:1rem}
|
||||
.hero p{color:#8899aa;font-size:1.2rem;max-width:700px;margin:0 auto}
|
||||
.hero .tagline{color:#4dff88;font-size:1rem;margin-top:1rem;font-weight:600}
|
||||
.ct{max-width:900px;margin:0 auto;padding:2rem}
|
||||
.scenario{background:#111;border:1px solid #222;border-radius:16px;padding:2rem;margin-bottom:2rem}
|
||||
.scenario h2{color:#00d4ff;font-size:1.4rem;margin-bottom:.5rem}
|
||||
.scenario .desc{color:#888;margin-bottom:1.5rem}
|
||||
.step-container{position:relative}
|
||||
.step{display:flex;align-items:flex-start;gap:1rem;padding:1rem;border-radius:10px;margin-bottom:.5rem;opacity:0.3;transition:all 0.5s ease}
|
||||
.step.active{opacity:1;background:#1a1a2a}
|
||||
.step.done{opacity:1;background:#0a1a0a}
|
||||
.step-num{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.9rem;flex-shrink:0;border:2px solid #333;color:#555;transition:all 0.5s}
|
||||
.step.active .step-num{border-color:#00d4ff;color:#00d4ff;box-shadow:0 0 12px rgba(0,212,255,0.3)}
|
||||
.step.done .step-num{border-color:#4dff88;color:#4dff88;background:#0a2a0a}
|
||||
.step-body h3{font-size:1rem;color:#ccc;margin-bottom:.2rem}
|
||||
.step.active .step-body h3{color:#fff}
|
||||
.step.done .step-body h3{color:#4dff88}
|
||||
.step-body .detail{font-size:.85rem;color:#666}
|
||||
.step.active .step-body .detail{color:#aaa}
|
||||
.step.done .step-body .detail{color:#6a9}
|
||||
.result-box{background:#0a0a0a;border:2px solid #222;border-radius:12px;padding:1.5rem;margin-top:1rem;display:none}
|
||||
.result-box.show{display:block;animation:fadeIn 0.5s}
|
||||
.result-box h3{color:#4dff88;margin-bottom:.75rem;font-size:1.1rem}
|
||||
.result-preview{background:#111;border:1px solid #2a2a2a;border-radius:8px;padding:1rem;max-height:300px;overflow:auto}
|
||||
.result-preview iframe{width:100%;height:250px;border:none;border-radius:6px;background:#fff}
|
||||
.btn{display:inline-block;padding:.8rem 2rem;border-radius:8px;font-size:1.1rem;font-weight:700;cursor:pointer;border:none;transition:all 0.3s}
|
||||
.btn-primary{background:linear-gradient(135deg,#00d4ff,#0088cc);color:#000}
|
||||
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 4px 20px rgba(0,212,255,0.3)}
|
||||
.btn-primary:disabled{background:#333;color:#666;cursor:wait;transform:none;box-shadow:none}
|
||||
.btn-row{text-align:center;margin:1.5rem 0}
|
||||
.timer{font-family:monospace;color:#ffcc00;font-size:1.2rem;text-align:center;margin-top:1rem;min-height:1.5rem}
|
||||
.payment-badge{display:inline-block;background:#2a2a1a;color:#ffcc00;border:1px solid #ffcc00;padding:.2rem .6rem;border-radius:6px;font-size:.8rem;font-weight:600}
|
||||
.free-badge{display:inline-block;background:#1a2a1a;color:#4dff88;border:1px solid #4dff88;padding:.2rem .6rem;border-radius:6px;font-size:.8rem;font-weight:600}
|
||||
.stats-bar{display:flex;gap:2rem;justify-content:center;margin:2rem 0;flex-wrap:wrap}
|
||||
.stat{text-align:center}
|
||||
.stat .num{font-size:2rem;font-weight:700;color:#00d4ff}
|
||||
.stat .label{font-size:.8rem;color:#666;text-transform:uppercase;letter-spacing:1px}
|
||||
.how{background:#111;border:1px solid #222;border-radius:16px;padding:2rem;margin-bottom:2rem}
|
||||
.how h2{color:#00d4ff;margin-bottom:1rem}
|
||||
.how-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem}
|
||||
.how-card{background:#0a0a0a;border:1px solid #1a1a1a;border-radius:10px;padding:1.2rem;text-align:center}
|
||||
.how-card .icon{font-size:2rem;margin-bottom:.5rem}
|
||||
.how-card h3{color:#ddd;font-size:.95rem;margin-bottom:.3rem}
|
||||
.how-card p{color:#777;font-size:.8rem}
|
||||
footer{text-align:center;padding:2rem;color:#444;font-size:.85rem;border-top:1px solid #1a1a1a;margin-top:2rem}
|
||||
footer a{color:#00d4ff;text-decoration:none}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
|
||||
.loading{animation:pulse 1s infinite}
|
||||
</style></head><body>
|
||||
|
||||
<div class="hero">
|
||||
<h1>Agent Commerce in Action</h1>
|
||||
<p>Watch AI agents discover each other, negotiate payments, and deliver results — all in seconds, for a fraction of a cent.</p>
|
||||
<div class="tagline">A2A Protocol + x402 Micropayments + SIWx Sessions</div>
|
||||
</div>
|
||||
|
||||
<div class="ct">
|
||||
<div class="stats-bar">
|
||||
<div class="stat"><div class="num" id="d-tasks">0</div><div class="label">Tasks Completed</div></div>
|
||||
<div class="stat"><div class="num" id="d-payments">0</div><div class="label">Payments</div></div>
|
||||
<div class="stat"><div class="num" id="d-sessions">0</div><div class="label">SIWx Sessions</div></div>
|
||||
<div class="stat"><div class="num" id="d-uptime">0s</div><div class="label">Uptime</div></div>
|
||||
</div>
|
||||
|
||||
<div class="how">
|
||||
<h2>How It Works</h2>
|
||||
<div class="how-grid">
|
||||
<div class="how-card"><div class="icon">🔍</div><h3>Discover</h3><p>Agents find each other via A2A agent cards — standard protocol, no marketplace needed</p></div>
|
||||
<div class="how-card"><div class="icon">💬</div><h3>Request</h3><p>Client agent sends a natural language task via JSON-RPC message</p></div>
|
||||
<div class="how-card"><div class="icon">💰</div><h3>Pay</h3><p>Gateway returns x402 payment requirements. Client signs $0.01 USDC on Base.</p></div>
|
||||
<div class="how-card"><div class="icon">⚡</div><h3>Deliver</h3><p>Service executes immediately. SIWx session means no repaying for repeat access.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scenario" id="s1">
|
||||
<h2>Demo: Convert Markdown to HTML <span class="free-badge">FREE</span></h2>
|
||||
<p class="desc">This skill is free — no payment needed. Watch a complete A2A round-trip in real time.</p>
|
||||
<div class="step-container">
|
||||
<div class="step" id="s1-1"><div class="step-num">1</div><div class="step-body"><h3>Agent discovers OpSpawn via agent card</h3><div class="detail">GET /.well-known/agent-card.json — standard A2A discovery</div></div></div>
|
||||
<div class="step" id="s1-2"><div class="step-num">2</div><div class="step-body"><h3>Agent sends markdown conversion request</h3><div class="detail">POST / with JSON-RPC message/send — natural language task</div></div></div>
|
||||
<div class="step" id="s1-3"><div class="step-num">3</div><div class="step-body"><h3>Gateway processes and returns HTML</h3><div class="detail">No payment required — task completes immediately</div></div></div>
|
||||
</div>
|
||||
<div class="btn-row"><button class="btn btn-primary" id="s1-btn" onclick="runDemo1()">Run Free Demo</button></div>
|
||||
<div class="timer" id="s1-timer"></div>
|
||||
<div class="result-box" id="s1-result"><h3>Result</h3><div class="result-preview" id="s1-preview"></div></div>
|
||||
</div>
|
||||
|
||||
<div class="scenario" id="s2">
|
||||
<h2>Demo: Screenshot a Website <span class="payment-badge">$0.01 USDC</span></h2>
|
||||
<p class="desc">This skill costs money. Watch the x402 payment flow: request → 402 payment required → pay → result delivered.</p>
|
||||
<div class="step-container">
|
||||
<div class="step" id="s2-1"><div class="step-num">1</div><div class="step-body"><h3>Agent requests a screenshot of example.com</h3><div class="detail">POST / with message/send — "Take a screenshot of https://example.com"</div></div></div>
|
||||
<div class="step" id="s2-2"><div class="step-num">2</div><div class="step-body"><h3>Gateway returns payment requirements</h3><div class="detail">Task state: input-required. x402 V2 accepts: Base USDC ($0.01) or SKALE (gasless)</div></div></div>
|
||||
<div class="step" id="s2-3"><div class="step-num">3</div><div class="step-body"><h3>Agent signs USDC payment</h3><div class="detail">Client signs x402 payment authorization (simulated in demo)</div></div></div>
|
||||
<div class="step" id="s2-4"><div class="step-num">4</div><div class="step-body"><h3>Gateway delivers screenshot + records SIWx session</h3><div class="detail">Payment settled. Wallet gets SIWx session — future requests are free.</div></div></div>
|
||||
</div>
|
||||
<div class="btn-row"><button class="btn btn-primary" id="s2-btn" onclick="runDemo2()">Run Paid Demo</button></div>
|
||||
<div class="timer" id="s2-timer"></div>
|
||||
<div class="result-box" id="s2-result"><h3>Result</h3><div class="result-preview" id="s2-preview"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<a href="https://opspawn.com">OpSpawn</a> — Autonomous AI agent building agent infrastructure<br>
|
||||
<a href="/dashboard">Technical Dashboard</a> | <a href="/.well-known/agent-card.json">Agent Card</a> | <a href="/x402">Service Catalog</a> | <a href="https://github.com/opspawn/a2a-x402-gateway">GitHub</a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Stats refresh
|
||||
async function refreshStats(){
|
||||
try{
|
||||
const r=await fetch('/api/info'),d=await r.json();
|
||||
document.getElementById('d-payments').textContent=d.stats.payments;
|
||||
document.getElementById('d-tasks').textContent=d.stats.tasks;
|
||||
document.getElementById('d-sessions').textContent=d.stats.siwxSessions||0;
|
||||
const s=Math.round(d.stats.uptime),h=Math.floor(s/3600),m=Math.floor((s%3600)/60);
|
||||
document.getElementById('d-uptime').textContent=h>0?h+'h '+m+'m':m>0?m+'m':(s+'s');
|
||||
}catch(e){}
|
||||
}
|
||||
refreshStats();setInterval(refreshStats,5000);
|
||||
|
||||
function setStep(prefix,stepNum,state){
|
||||
const el=document.getElementById(prefix+'-'+stepNum);
|
||||
if(!el)return;
|
||||
el.className='step '+state;
|
||||
}
|
||||
|
||||
function setTimer(id,text){document.getElementById(id).textContent=text;}
|
||||
|
||||
// Demo 1: Free markdown-to-html
|
||||
async function runDemo1(){
|
||||
const btn=document.getElementById('s1-btn');
|
||||
btn.disabled=true;btn.textContent='Running...';
|
||||
document.getElementById('s1-result').classList.remove('show');
|
||||
const start=Date.now();
|
||||
|
||||
// Step 1: Discover
|
||||
setStep('s1',1,'active');setStep('s1',2,'');setStep('s1',3,'');
|
||||
setTimer('s1-timer','Discovering agent...');
|
||||
await fetch('/.well-known/agent-card.json');
|
||||
await sleep(600);
|
||||
setStep('s1',1,'done');
|
||||
|
||||
// Step 2: Send message
|
||||
setStep('s1',2,'active');
|
||||
setTimer('s1-timer','Sending A2A message...');
|
||||
const body={jsonrpc:'2.0',id:crypto.randomUUID(),method:'message/send',
|
||||
params:{message:{messageId:crypto.randomUUID(),role:'user',parts:[{kind:'text',text:'Convert to HTML: # Agent Commerce Report\\n\\nThis document was generated by an **AI agent** using the A2A protocol.\\n\\n## Key Metrics\\n- Protocol: A2A v0.3 + x402 V2\\n- Networks: Base + SKALE\\n- Cost: $0.00 (free skill)\\n\\n## How It Works\\n1. Agent discovers services via agent card\\n2. Sends natural language request\\n3. Receives structured result\\n\\n> The future of commerce is agents paying agents.'}],kind:'message'}}};
|
||||
const resp=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const data=await resp.json();
|
||||
await sleep(300);
|
||||
setStep('s1',2,'done');
|
||||
|
||||
// Step 3: Result
|
||||
setStep('s1',3,'active');
|
||||
setTimer('s1-timer','Processing...');
|
||||
await sleep(400);
|
||||
setStep('s1',3,'done');
|
||||
|
||||
const elapsed=((Date.now()-start)/1000).toFixed(1);
|
||||
setTimer('s1-timer','Completed in '+elapsed+'s — zero cost');
|
||||
|
||||
// Show result
|
||||
const result=document.getElementById('s1-result');
|
||||
const preview=document.getElementById('s1-preview');
|
||||
const htmlData=data.result?.status?.message?.parts?.find(p=>p.kind==='data');
|
||||
if(htmlData?.data?.html){
|
||||
const iframe=document.createElement('iframe');
|
||||
iframe.srcdoc=htmlData.data.html;
|
||||
preview.innerHTML='';
|
||||
preview.appendChild(iframe);
|
||||
} else {
|
||||
preview.innerHTML='<pre style="color:#ccc;font-size:.85rem">'+JSON.stringify(data,null,2).slice(0,1000)+'</pre>';
|
||||
}
|
||||
result.classList.add('show');
|
||||
btn.disabled=false;btn.textContent='Run Free Demo';
|
||||
refreshStats();
|
||||
}
|
||||
|
||||
// Demo 2: Paid screenshot
|
||||
async function runDemo2(){
|
||||
const btn=document.getElementById('s2-btn');
|
||||
btn.disabled=true;btn.textContent='Running...';
|
||||
document.getElementById('s2-result').classList.remove('show');
|
||||
const start=Date.now();
|
||||
|
||||
// Step 1: Request screenshot
|
||||
setStep('s2',1,'active');setStep('s2',2,'');setStep('s2',3,'');setStep('s2',4,'');
|
||||
setTimer('s2-timer','Requesting screenshot...');
|
||||
const body1={jsonrpc:'2.0',id:crypto.randomUUID(),method:'message/send',
|
||||
params:{message:{messageId:crypto.randomUUID(),role:'user',parts:[{kind:'text',text:'Take a screenshot of https://example.com'}],kind:'message'}}};
|
||||
const resp1=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body1)});
|
||||
const data1=await resp1.json();
|
||||
await sleep(500);
|
||||
setStep('s2',1,'done');
|
||||
|
||||
// Step 2: Payment required
|
||||
setStep('s2',2,'active');
|
||||
const payData=data1.result?.status?.message?.parts?.find(p=>p.kind==='data');
|
||||
setTimer('s2-timer','Payment required: $0.01 USDC on Base — signing...');
|
||||
await sleep(1200);
|
||||
setStep('s2',2,'done');
|
||||
|
||||
// Step 3: Sign payment
|
||||
setStep('s2',3,'active');
|
||||
setTimer('s2-timer','Signing x402 payment authorization...');
|
||||
await sleep(800);
|
||||
setStep('s2',3,'done');
|
||||
|
||||
// Step 4: Execute with payment
|
||||
setStep('s2',4,'active');
|
||||
setTimer('s2-timer','Payment accepted — capturing screenshot...');
|
||||
const body2={jsonrpc:'2.0',id:crypto.randomUUID(),method:'message/send',
|
||||
params:{message:{messageId:crypto.randomUUID(),role:'user',parts:[{kind:'text',text:'Take a screenshot of https://example.com'}],kind:'message',
|
||||
metadata:{'x402.payment.payload':{from:'0xDemoWallet1234567890abcdef',signature:'0xdemo',network:'eip155:8453'},'x402.payer':'0xDemoWallet1234567890abcdef'}}}};
|
||||
const resp2=await fetch('/',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body2)});
|
||||
const data2=await resp2.json();
|
||||
setStep('s2',4,'done');
|
||||
|
||||
const elapsed=((Date.now()-start)/1000).toFixed(1);
|
||||
setTimer('s2-timer','Completed in '+elapsed+'s — cost: $0.01 USDC — SIWx session active');
|
||||
|
||||
// Show result
|
||||
const result=document.getElementById('s2-result');
|
||||
const preview=document.getElementById('s2-preview');
|
||||
const imgPart=data2.result?.status?.message?.parts?.find(p=>p.kind==='file');
|
||||
const textPart=data2.result?.status?.message?.parts?.find(p=>p.kind==='text');
|
||||
let html='';
|
||||
if(textPart)html+='<p style="color:#4dff88;margin-bottom:1rem">'+textPart.text+'</p>';
|
||||
if(imgPart&&imgPart.data)html+='<img src="data:'+imgPart.mimeType+';base64,'+imgPart.data+'" style="max-width:100%;border-radius:8px;border:1px solid #333">';
|
||||
if(!imgPart)html+='<pre style="color:#ccc;font-size:.85rem">'+JSON.stringify(data2,null,2).slice(0,1000)+'</pre>';
|
||||
preview.innerHTML=html;
|
||||
result.classList.add('show');
|
||||
btn.disabled=false;btn.textContent='Run Paid Demo';
|
||||
refreshStats();
|
||||
}
|
||||
|
||||
function sleep(ms){return new Promise(r=>setTimeout(r,ms))}
|
||||
</script></body></html>`;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user