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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 62 additions & 76 deletions app/api/agents/create/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ function isMissingTableError(error: { message?: string; code?: string } | null |
if (!error) return false;
const message = (error.message || '').toLowerCase();
return message.includes("could not find the table 'public.agents'")
|| message.includes("could not find the table 'public.users'")
|| message.includes('relation "public.agents" does not exist')
|| message.includes('relation "public.users" does not exist')
|| error.code === 'PGRST205';
|| error.code === 'PGRST205'
|| error.code === '42P01';
}

function getSupabase() {
Expand Down Expand Up @@ -68,70 +67,9 @@ export async function POST(req: NextRequest) {
const apiEndpoint = `${origin}/api/agents/${agentId}/run`;

const canUseSupabase = Boolean(supabaseUrl && supabaseWriteKey);
const supabase = canUseSupabase ? getSupabase() : null;

const ensureUser = async () => {
if (!supabase) return { message: 'Supabase not configured', code: 'NO_SUPABASE' };

const upsertRes = await supabase
.from('users')
.upsert({ wallet_address: owner_wallet }, { onConflict: 'wallet_address' });

// Some DBs may miss a unique constraint on wallet_address; fallback to insert.
if (upsertRes.error?.code === '42P10') {
const insertRes = await supabase
.from('users')
.insert({ wallet_address: owner_wallet });

if (insertRes.error && insertRes.error.code !== '23505') {
return insertRes.error;
}
return null;
}

if (upsertRes.error && upsertRes.error.code !== '23505') {
return upsertRes.error;
}

return null;
};

const insertAgent = async () => {
if (!supabase) return { data: null, error: { message: 'Supabase not configured', code: 'NO_SUPABASE' } };

return supabase.from('agents').insert({
id: agentId,
owner_wallet,
name,
description,
tags: tags || [],
model,
system_prompt,
tools: tools || [],
price_xlm: parseFloat(price_xlm) || 0.01,
visibility: visibility || 'public',
api_endpoint: apiEndpoint,
api_key: apiKey,
});
};

const userError = await ensureUser();
if (userError && !isMissingTableError(userError) && userError.code !== 'NO_SUPABASE') {
console.error('Supabase user upsert error:', userError);
}

let { error: agentError } = await insertAgent();

// Retry once when FK fails due owner row race/order issues.
if (agentError?.code === '23503') {
const retryUserError = await ensureUser();
if (retryUserError) {
console.error('Supabase user upsert retry error:', retryUserError);
}
({ error: agentError } = await insertAgent());
}

if (agentError && (agentError.code === 'NO_SUPABASE' || isMissingTableError(agentError))) {
if (!canUseSupabase) {
// Supabase not configured – use local demo store
upsertDemoAgent({
id: agentId,
owner_wallet,
Expand All @@ -146,35 +84,83 @@ export async function POST(req: NextRequest) {
api_endpoint: apiEndpoint,
api_key: apiKey,
});

return NextResponse.json({
id: agentId,
api_key: apiKey,
api_endpoint: apiEndpoint,
message: 'Agent deployed using fallback storage (Supabase tables not found)',
message: 'Agent deployed (local demo mode – configure Supabase env vars to persist)',
storage_mode: 'demo_fallback',
warning: 'Apply supabase-schema.sql to persist agents in database',
});
}

const supabase = getSupabase();

// Also upsert wallet into users table (best-effort; failures are non-fatal
// because agents table no longer has an FK to users).
try {
await supabase
.from('users')
.upsert({ wallet_address: owner_wallet }, { onConflict: 'wallet_address' });
} catch (err) {
// Non-fatal: users table may not exist yet
console.debug('[create] User upsert skipped:', err);
}

const { error: agentError } = await supabase.from('agents').insert({
id: agentId,
owner_wallet,
name,
description,
tags: tags || [],
model,
system_prompt,
tools: tools || [],
price_xlm: parseFloat(price_xlm) || 0.01,
visibility: visibility || 'public',
api_endpoint: apiEndpoint,
api_key: apiKey,
});

if (agentError) {
console.error('Supabase agent insert error:', agentError);
if (agentError.code === '23503') {
return NextResponse.json(
{ error: 'Failed to persist deployed agent: owner wallet is not available in users table', key_mode: keyMode },
{ status: 500 }
);
if (isMissingTableError(agentError)) {
// Tables not yet created – fall back to local demo store and inform caller
upsertDemoAgent({
id: agentId,
owner_wallet,
name,
description,
tags: tags || [],
model,
system_prompt,
tools: tools || [],
price_xlm: parseFloat(price_xlm) || 0.01,
visibility: visibility || 'public',
api_endpoint: apiEndpoint,
api_key: apiKey,
});
return NextResponse.json({
id: agentId,
api_key: apiKey,
api_endpoint: apiEndpoint,
message: 'Agent deployed (demo fallback – run supabase-schema.sql in your Supabase SQL editor to persist agents)',
storage_mode: 'demo_fallback',
warning: 'Apply supabase-schema.sql to persist agents in database',
});
}

console.error('Supabase agent insert error:', agentError);

if (agentError.code === '42501') {
return NextResponse.json(
{
error: 'Failed to persist deployed agent: database permission denied (check SUPABASE_SERVICE_ROLE_KEY and RLS policies)',
error: 'Database permission denied check SUPABASE_SERVICE_ROLE_KEY and that RLS is disabled on the agents table',
details: agentError.message,
key_mode: keyMode,
},
{ status: 500 }
);
}

return NextResponse.json(
{
error: 'Failed to persist deployed agent',
Expand Down
109 changes: 109 additions & 0 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,115 @@ export default function DashboardPage() {
</div>
</div>

{/* Traders Portfolio */}
<div className="p-5 rounded-2xl border border-[rgba(0,255,229,0.1)] bg-[rgba(0,255,229,0.02)]">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-syne text-lg font-bold text-white">Traders Portfolio</h3>
<p className="font-mono text-[10px] text-gray-500 mt-0.5">Real-time P&amp;L per transaction · agent strategy tracking</p>
</div>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${feedConnected ? 'bg-[#00FFE5] animate-pulse' : 'bg-amber-400'}`} />
<span className="font-mono text-[10px] text-gray-500">{feedConnected ? 'live' : 'offline'}</span>
</div>
</div>

{/* Portfolio summary row */}
<div className="grid grid-cols-3 gap-3 mb-5">
<div className="rounded-xl border border-[rgba(74,222,128,0.2)] bg-[rgba(74,222,128,0.04)] p-3 text-center">
<div className="font-syne text-xl font-bold text-[#4ade80]">+{totalEarned.toFixed(4)}</div>
<div className="font-mono text-[10px] text-gray-500 mt-0.5">Total Profit (XLM)</div>
{totalEarnedUsd && <div className="font-mono text-[10px] text-[#4ade80]/70">≈ ${totalEarnedUsd.toFixed(2)}</div>}
</div>
<div className="rounded-xl border border-[rgba(255,255,255,0.06)] bg-[rgba(255,255,255,0.02)] p-3 text-center">
<div className="font-syne text-xl font-bold text-[#FFB800]">{paidRequests}</div>
<div className="font-mono text-[10px] text-gray-500 mt-0.5">Paid Trades</div>
</div>
<div className="rounded-xl border border-[rgba(255,255,255,0.06)] bg-[rgba(255,255,255,0.02)] p-3 text-center">
<div className="font-syne text-xl font-bold text-purple-400">
{paidRequests > 0 ? (totalEarned / paidRequests).toFixed(4) : '0.0000'}
</div>
<div className="font-mono text-[10px] text-gray-500 mt-0.5">Avg per Trade (XLM)</div>
</div>
</div>

{/* Trade rows */}
<div className="overflow-x-auto">
<table className="w-full min-w-[680px]">
<thead>
<tr className="border-b border-white/[0.06]">
{['#', 'Agent / Strategy', 'Model', 'P&L (XLM)', 'Tx', 'Time'].map((h) => (
<th key={h} className="py-2 pr-4 text-left font-mono text-[10px] text-gray-500 uppercase">{h}</th>
))}
</tr>
</thead>
<tbody>
{(analytics?.invoices || []).length === 0 && (
<tr>
<td colSpan={6} className="py-6 text-center font-mono text-xs text-white/30">
No trades yet — run a paid agent request to populate the portfolio.
</td>
</tr>
)}
{(analytics?.invoices || []).map((row, idx) => (
<tr key={row.invoiceId} className="border-b border-white/[0.03] hover:bg-white/[0.02] transition-colors">
<td className="py-2.5 pr-4 font-mono text-[10px] text-gray-600">{idx + 1}</td>
<td className="py-2.5 pr-4">
<div className="font-mono text-xs text-white">{row.agentName}</div>
<div className="font-mono text-[9px] text-gray-600 mt-0.5">via 0x402 protocol</div>
</td>
<td className="py-2.5 pr-4">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-mono border ${
row.model === 'openai-gpt4o-mini'
? 'border-[rgba(0,255,229,0.3)] text-[#00FFE5] bg-[rgba(0,255,229,0.06)]'
: 'border-purple-800 text-purple-400 bg-purple-900/20'
}`}>
{modelName(row.model)}
</span>
</td>
<td className="py-2.5 pr-4">
<span className="font-mono text-xs text-[#4ade80] font-bold">+{row.amountXlm.toFixed(4)}</span>
</td>
<td className="py-2.5 pr-4">
<a
href={row.txExplorerUrl}
target="_blank"
rel="noreferrer"
className="font-mono text-[10px] text-[#FFB800] hover:underline"
>
{row.txHash ? `${row.txHash.slice(0, 8)}…` : '—'}
</a>
</td>
<td className="py-2.5 pr-4 font-mono text-[10px] text-white/40">
{new Date(row.createdAt).toLocaleTimeString([], { hour12: false })}
</td>
</tr>
))}
</tbody>
</table>
</div>

{/* Live new trades from Ably */}
{(() => {
const liveTrades = myLiveEvents.filter((e) => (e.priceXlm ?? 0) > 0).slice(0, 5);
if (liveTrades.length === 0) return null;
return (
<div className="mt-4 pt-4 border-t border-white/[0.06]">
<p className="font-mono text-[10px] text-gray-500 mb-2 uppercase tracking-widest">New (live)</p>
{liveTrades.map((ev, idx) => (
<div key={`live-${idx}`} className="flex items-center justify-between py-1.5">
<div className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-[#00FFE5] animate-pulse shrink-0" />
<span className="font-mono text-xs text-white/70">{ev.agentName}</span>
</div>
<span className="font-mono text-xs text-[#4ade80]">+{(ev.priceXlm ?? 0).toFixed(4)} XLM</span>
</div>
))}
</div>
);
})()}
</div>

{/* Invoice Stream */}
<div className="p-5 rounded-2xl border border-[rgba(255,255,255,0.08)] bg-[rgba(255,255,255,0.02)]">
<div className="flex items-center justify-between mb-3">
Expand Down
5 changes: 4 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import Navbar from "@/components/Navbar";
import AppShell from "@/components/AppShell";

export const metadata: Metadata = {
title: "AgentForge — AI Agent Marketplace on Stellar",
Expand All @@ -21,7 +22,9 @@ export default function RootLayout({
</head>
<body className="font-sans antialiased bg-[#050508] text-white">
<Navbar />
<main className="pt-16">{children}</main>
<main className="pt-16">
<AppShell>{children}</AppShell>
</main>
</body>
</html>
);
Expand Down
Loading
Loading