From 75188136587573d408fca3dba3dcc2b0c8d759b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:44:01 +0000 Subject: [PATCH 1/2] Initial plan From 15d7e92a7b5926bc09493d1dfa8bdeb003bf466f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 03:41:09 +0000 Subject: [PATCH 2/2] Fix payment flow, add candlestick chart with real-time PnL, improve dashboard Agent-Logs-Url: https://github.com/mesayanroy/0x402-pubsub/sessions/cda26531-4dc7-461c-89d0-54844912e711 Co-authored-by: mesayanroy <169074736+mesayanroy@users.noreply.github.com> --- app/agents/[id]/page.tsx | 8 +- app/api/agents/[id]/run/route.ts | 33 ++++-- app/dashboard/page.tsx | 77 +++++++++++-- app/trading/page.tsx | 119 ++++++++++++++------ components/AgentBuilder.tsx | 35 +++++- components/CandlestickChart.tsx | 185 +++++++++++++++++++++++++++++++ components/PaymentModal.tsx | 4 +- lib/stellar.ts | 21 +++- 8 files changed, 416 insertions(+), 66 deletions(-) create mode 100644 components/CandlestickChart.tsx diff --git a/app/agents/[id]/page.tsx b/app/agents/[id]/page.tsx index 33aae46..3427d01 100644 --- a/app/agents/[id]/page.tsx +++ b/app/agents/[id]/page.tsx @@ -53,11 +53,11 @@ export default function AgentDetailPage() { if (agentId) fetchAgent(); }, [agentId]); - const runAgent = async (txHash?: string) => { + const runAgent = async (txHash?: string, signerWallet?: string) => { setRunning(true); setError(null); try { - const walletAddress = localStorage.getItem('wallet_address'); + const walletAddress = signerWallet || localStorage.getItem('wallet_address'); const headers: Record = { 'Content-Type': 'application/json' }; if (txHash) { headers['X-Payment-Tx-Hash'] = txHash; @@ -262,9 +262,9 @@ X-Payment-Wallet: {your_G_address} priceXlm={paymentChallenge?.amountXlm ?? agent.price_xlm} ownerAddress={paymentChallenge?.address ?? agent.owner_wallet} paymentMemo={paymentChallenge?.memo ?? `agent:${agent.id}`} - onPaymentSuccess={(txHash) => { + onPaymentSuccess={(txHash, signerWallet) => { setPaymentModal(false); - runAgent(txHash); + runAgent(txHash, signerWallet); }} /> diff --git a/app/api/agents/[id]/run/route.ts b/app/api/agents/[id]/run/route.ts index def9ef0..cec3bad 100644 --- a/app/api/agents/[id]/run/route.ts +++ b/app/api/agents/[id]/run/route.ts @@ -44,12 +44,28 @@ async function getAgent(agentId: string) { async function runAgentModel(model: string, systemPrompt: string, userInput: string): Promise { if (model === 'openai-gpt4o-mini') { - const { runOpenAIAgent } = await import('@/lib/openai'); - return runOpenAIAgent(systemPrompt, userInput); + if (!process.env.OPENAI_API_KEY) { + return '[Demo mode] OpenAI API key not configured. Your agent received the input and would normally respond here. Set OPENAI_API_KEY to enable live AI responses.'; + } + try { + const { runOpenAIAgent } = await import('@/lib/openai'); + return runOpenAIAgent(systemPrompt, userInput); + } catch (err) { + console.error('[run] OpenAI model error:', err); + return `[AI Error] The agent model returned an error: ${String(err)}. Payment was processed successfully.`; + } } if (model === 'anthropic-claude-haiku') { - const { runAnthropicAgent } = await import('@/lib/anthropic'); - return runAnthropicAgent(systemPrompt, userInput); + if (!process.env.ANTHROPIC_API_KEY) { + return '[Demo mode] Anthropic API key not configured. Your agent received the input and would normally respond here. Set ANTHROPIC_API_KEY to enable live AI responses.'; + } + try { + const { runAnthropicAgent } = await import('@/lib/anthropic'); + return runAnthropicAgent(systemPrompt, userInput); + } catch (err) { + console.error('[run] Anthropic model error:', err); + return `[AI Error] The agent model returned an error: ${String(err)}. Payment was processed successfully.`; + } } return 'Unknown model'; } @@ -154,13 +170,6 @@ export async function POST( const requestId = uuidv4(); if (paymentTxHash && agent.price_xlm > 0) { - if (!callerWallet) { - return NextResponse.json( - { error: 'Missing X-Payment-Wallet header for paid request' }, - { status: 400 } - ); - } - // Verify paid request inline so API callers get immediate completion even // when background consumers are not running. const paymentVerified = await verifyPayment( @@ -168,7 +177,7 @@ export async function POST( agent.owner_wallet, agent.price_xlm, agentId, - callerWallet + callerWallet || undefined ); if (!paymentVerified) { return NextResponse.json({ error: 'Payment verification failed' }, { status: 402 }); diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index c1ef988..43408ac 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -18,6 +18,8 @@ import { YAxis, } from 'recharts'; import { Agent } from '@/types'; +import { useMarketplaceFeed } from '@/hooks/useMarketplaceFeed'; +import { truncateAddress } from '@/lib/stellar'; type AnalyticsResponse = { byModel: Array<{ model: string; requests: number; paidRequests: number; earnedXlm: number; avgLatencyMs: number }>; @@ -77,6 +79,11 @@ export default function DashboardPage() { const [loading, setLoading] = useState(true); const [xlmPrice, setXlmPrice] = useState(null); + // Real-time live feed of 0x402 activity for MY agents + const { events: liveEvents, isConnected: feedConnected } = useMarketplaceFeed({ maxEvents: 20 }); + const myAgentIds = new Set(myAgents.map((a) => a.id)); + const myLiveEvents = liveEvents.filter((e) => myAgentIds.has(e.agentId)); + useEffect(() => { const addr = localStorage.getItem('wallet_address'); if (!addr) return; @@ -372,6 +379,51 @@ export default function DashboardPage() { + {/* Real-time 0x402 Live Activity */} +
+
+
+ + Live 0x402 Activity +
+ {feedConnected ? 'connected · Ably' : 'reconnecting...'} +
+
+ {myLiveEvents.length === 0 ? ( +
+ No live activity yet. Real-time events will appear here when your agents are called. +
+ ) : ( + myLiveEvents.slice(0, 10).map((ev, idx) => ( +
+
+ + {ev.eventType.replace(/_/g, '\u00A0')} + + {ev.agentName} + + {ev.callerWallet ? truncateAddress(ev.callerWallet) : 'anonymous'} + +
+
+ {typeof ev.priceXlm === 'number' && ev.priceXlm > 0 && ( + +{ev.priceXlm.toFixed(4)} XLM + )} + {ev.txHash && ( + + {ev.txHash.slice(0, 8)}… + + )} + + {new Date(ev.timestamp).toLocaleTimeString('en-US', { hour12: false })} + +
+
+ )) + )} +
+
+ {/* My Agents */}
@@ -382,22 +434,33 @@ export default function DashboardPage() {
{myAgents.length === 0 && ( -

No agents deployed yet.

+

No agents deployed yet. Build your first agent →

)} {myAgents.map((agent) => ( -
+
-
{agent.name}
+
+
{agent.name}
+ {agent.forked_from && ( + forked + )} + {agent.visibility === 'public' && !agent.forked_from && ( + marketplace + )} +
{agent.model === 'openai-gpt4o-mini' ? 'GPT-4o Mini' : 'Claude Haiku'} · {agent.price_xlm} XLM/req · {agent.visibility}
+ {agent.forked_from && ( +
Forked · ID: {agent.forked_from.slice(0, 8)}…
+ )}
-
{agent.total_earned_xlm} XLM
- {xlmPrice &&
≈ ${(agent.total_earned_xlm * xlmPrice).toFixed(2)}
} -
{agent.total_requests.toLocaleString()} requests
+
{(agent.total_earned_xlm ?? 0).toFixed(4)} XLM
+ {xlmPrice &&
≈ ${((agent.total_earned_xlm ?? 0) * xlmPrice).toFixed(2)}
} +
{(agent.total_requests ?? 0).toLocaleString()} requests
-
+ ))}
diff --git a/app/trading/page.tsx b/app/trading/page.tsx index 23a24a3..45a8fec 100644 --- a/app/trading/page.tsx +++ b/app/trading/page.tsx @@ -3,17 +3,14 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { - Area, - AreaChart, Bar, BarChart, - CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, - ReferenceLine, } from 'recharts'; +import CandlestickChart from '@/components/CandlestickChart'; interface OHLC { ts: string; @@ -43,10 +40,12 @@ interface Position { collateral: number; leverage: number; unrealisedPnl: number; + realisedPnl?: number; liquidationPrice: number; tp: number | null; sl: number | null; pair: string; + openedAt: string; } interface TokenInfo { @@ -137,6 +136,7 @@ export default function TradingPage() { const [collateral, setCollateral] = useState('50'); const [orders, setOrders] = useState([]); const [position, setPosition] = useState(null); + const [closedPnl, setClosedPnl] = useState<{ pnl: number; pair: string; ts: string } | null>(null); const [activeTab, setActiveTab] = useState<'chart' | 'agents'>('chart'); const [selectedAgent, setSelectedAgent] = useState(null); const [agentCategory, setAgentCategory] = useState('all'); @@ -147,6 +147,12 @@ export default function TradingPage() { const tickerRef = useRef | null>(null); const priceIntervalRef = useRef | null>(null); + const positionRef = useRef(null); + + // Keep positionRef in sync so PnL effect always reads fresh position + useEffect(() => { + positionRef.current = position; + }, [position]); const selectedPair = TOKEN_PAIRS.find((p) => p.id === selectedPairId) ?? TOKEN_PAIRS[0]; const currentTokenInfo = tokenPrices[selectedPair.coinGeckoId]; @@ -245,6 +251,53 @@ export default function TradingPage() { const recentLow = candles.length ? Math.min(...candles.slice(-20).map((c) => c.low)) : 0; const chartData = candles.slice(-40).map((c) => ({ ts: c.ts, price: c.close, high: c.high, low: c.low, volume: c.volume })); + // Real-time PnL: update unrealisedPnl whenever the live candle price changes. + // Use positionRef to always read the latest position without causing an infinite + // re-render loop (adding `position` to deps would trigger the effect on every PnL + // update, which in turn re-renders causing another candle-change cycle). + useEffect(() => { + const pos = positionRef.current; + if (!pos) return; + const livePrice = candles[candles.length - 1]?.close; + if (!livePrice) return; + const priceDiff = pos.side === 'long' + ? livePrice - pos.entryPrice + : pos.entryPrice - livePrice; + const pnl = priceDiff * pos.size * pos.leverage; + + // Check TP / SL triggers + if (pos.tp && pos.side === 'long' && livePrice >= pos.tp) { + const tpPnl = (pos.tp - pos.entryPrice) * pos.size * pos.leverage; + setClosedPnl({ pnl: tpPnl, pair: pos.pair, ts: new Date().toISOString() }); + setOrderSuccess(`🎯 Take Profit hit! PnL: +$${fmtPrice(tpPnl)} · ${pos.pair.toUpperCase()}`); + setPosition(null); + return; + } + if (pos.tp && pos.side === 'short' && livePrice <= pos.tp) { + const tpPnl = (pos.entryPrice - pos.tp) * pos.size * pos.leverage; + setClosedPnl({ pnl: tpPnl, pair: pos.pair, ts: new Date().toISOString() }); + setOrderSuccess(`🎯 Take Profit hit! PnL: +$${fmtPrice(tpPnl)} · ${pos.pair.toUpperCase()}`); + setPosition(null); + return; + } + if (pos.sl && pos.side === 'long' && livePrice <= pos.sl) { + const slPnl = (pos.sl - pos.entryPrice) * pos.size * pos.leverage; + setClosedPnl({ pnl: slPnl, pair: pos.pair, ts: new Date().toISOString() }); + setOrderSuccess(`🛑 Stop Loss triggered. PnL: ${slPnl >= 0 ? '+' : ''}$${fmtPrice(slPnl)} · ${pos.pair.toUpperCase()}`); + setPosition(null); + return; + } + if (pos.sl && pos.side === 'short' && livePrice >= pos.sl) { + const slPnl = (pos.entryPrice - pos.sl) * pos.size * pos.leverage; + setClosedPnl({ pnl: slPnl, pair: pos.pair, ts: new Date().toISOString() }); + setOrderSuccess(`🛑 Stop Loss triggered. PnL: ${slPnl >= 0 ? '+' : ''}$${fmtPrice(slPnl)} · ${pos.pair.toUpperCase()}`); + setPosition(null); + return; + } + + setPosition((prev) => prev ? { ...prev, unrealisedPnl: pnl } : null); + }, [candles]); + const submitOrder = useCallback(() => { setOrderError(null); setOrderSuccess(null); const amt = parseFloat(orderAmount); @@ -260,7 +313,7 @@ export default function TradingPage() { setOrders((prev) => [newOrder, ...prev.slice(0, 19)]); if (orderType === 'market') { const liqOffset = col / (amt * leverage) * (orderSide === 'buy' ? -1 : 1); - setPosition({ side: orderSide === 'buy' ? 'long' : 'short', entryPrice: price, size: amt, collateral: col, leverage, unrealisedPnl: 0, liquidationPrice: price + liqOffset, tp, sl, pair: selectedPairId }); + setPosition({ side: orderSide === 'buy' ? 'long' : 'short', entryPrice: price, size: amt, collateral: col, leverage, unrealisedPnl: 0, liquidationPrice: price + liqOffset, tp, sl, pair: selectedPairId, openedAt: new Date().toISOString() }); setOrderSuccess(`Market ${orderSide.toUpperCase()} filled @ $${fmtPrice(price)} · ${selectedPair.symbol}`); } else { setOrderSuccess(`Limit order placed @ $${fmtPrice(price)} · ${selectedPair.symbol}`); @@ -269,7 +322,9 @@ export default function TradingPage() { const closePosition = () => { if (!position) return; - setOrderSuccess(`Position closed. PnL: ${position.unrealisedPnl >= 0 ? '+' : ''}$${fmtPrice(position.unrealisedPnl)}`); + const pnl = position.unrealisedPnl; + setClosedPnl({ pnl, pair: position.pair, ts: new Date().toISOString() }); + setOrderSuccess(`Position closed. PnL: ${pnl >= 0 ? '+' : ''}$${fmtPrice(pnl)} · ${position.pair.toUpperCase()}`); setPosition(null); }; @@ -347,42 +402,40 @@ export default function TradingPage() {
- {/* Price Chart */} + {/* Closed PnL banner */} + + {closedPnl && ( + = 0 ? 'border-green-800 bg-[rgba(74,222,128,0.08)] text-[#4ade80]' : 'border-red-900 bg-red-900/10 text-red-400'}`}> + {closedPnl.pnl >= 0 ? '🟢 Profit' : '🔴 Loss'} on {closedPnl.pair.toUpperCase()} — {closedPnl.pnl >= 0 ? '+' : ''}${fmtPrice(closedPnl.pnl)} + + + )} + + + {/* Candlestick Price Chart */}
Live · 1m candles · {selectedPair.symbol} + 🟢 bullish 🔴 bearish
- — Resistance {fmtPrice(recentHigh)} - — Support {fmtPrice(recentLow)} + — RES {fmtPrice(recentHigh)} + — SUP {fmtPrice(recentLow)}
-
- - - - - - - - - - new Date(v).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })} /> - fmtPrice(v)} width={72} /> - new Date(String(l)).toLocaleTimeString([], { hour12: false })} - formatter={(v: unknown) => [`$${fmtPrice(v as number)}`, 'Price']} /> - - - {position?.tp && } - {position?.sl && } - {position && } - - - -
+
{/* Volume Bar */} diff --git a/components/AgentBuilder.tsx b/components/AgentBuilder.tsx index 6a069e6..8bda2d8 100644 --- a/components/AgentBuilder.tsx +++ b/components/AgentBuilder.tsx @@ -15,6 +15,7 @@ interface AgentFormData { tools: string[]; priceXlm: string; visibility: 'public' | 'private' | 'forked'; + listInMarketplace: boolean; } const DRAFT_KEY = 'agent_builder_draft'; @@ -29,6 +30,7 @@ const initialData: AgentFormData = { tools: [], priceXlm: '0.01', visibility: 'public', + listInMarketplace: true, }; const toolOptions = [ @@ -137,7 +139,8 @@ export default function AgentBuilder() { system_prompt: form.systemPrompt, tools: form.tools, price_xlm: parseFloat(form.priceXlm), - visibility: form.visibility, + visibility: form.listInMarketplace ? 'public' : form.visibility, + list_in_marketplace: form.listInMarketplace, }), }); @@ -306,6 +309,28 @@ export default function AgentBuilder() {
+ + {/* Marketplace listing toggle */} +
setForm((prev) => ({ ...prev, listInMarketplace: !prev.listInMarketplace }))} + className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-all ${ + form.listInMarketplace + ? 'border-[rgba(0,255,229,0.35)] bg-[rgba(0,255,229,0.06)]' + : 'border-[rgba(255,255,255,0.08)] bg-transparent' + }`} + > +
+ {form.listInMarketplace && } +
+
+
List in Marketplace for monetization
+
+ Your agent will appear in the public marketplace. Earn {form.priceXlm || '0.01'} XLM per request via 0x402 protocol. +
+
+