squalid-singularity 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.vscode/extensions.json +4 -0
  2. package/.vscode/launch.json +11 -0
  3. package/.wrangler/tmp/pages-pHhhPx/_routes-0.7693472831665579.json +9 -0
  4. package/.wrangler/tmp/pages-pHhhPx/functions-filepath-routing-config-0.7436749681606077.json +21 -0
  5. package/.wrangler/tmp/pages-pHhhPx/functionsRoutes-0.14872757927825653.mjs +19 -0
  6. package/.wrangler/tmp/pages-pHhhPx/functionsWorker-0.7091847872345003.js +491 -0
  7. package/.wrangler/tmp/pages-yKW4pG/_routes-0.6780167228686584.json +9 -0
  8. package/.wrangler/tmp/pages-yKW4pG/functions-filepath-routing-config-0.6268818876758142.json +21 -0
  9. package/.wrangler/tmp/pages-yKW4pG/functionsRoutes-0.016215448179317304.mjs +19 -0
  10. package/.wrangler/tmp/pages-yKW4pG/functionsWorker-0.29714428274758986.js +491 -0
  11. package/README.md +43 -0
  12. package/astro.config.mjs +26 -0
  13. package/functions/agent/[[path]].ts +9 -0
  14. package/functions/starlight/[[path]].ts +9 -0
  15. package/functions/task/[[path]].ts +9 -0
  16. package/index.html.bak +1755 -0
  17. package/package.json +24 -0
  18. package/public/_redirects +1 -0
  19. package/public/art/hero.webp +0 -0
  20. package/public/favicon.ico +0 -0
  21. package/public/favicon.svg +5 -0
  22. package/public/images/generated/01-red-cube-editorial.png +0 -0
  23. package/public/images/generated/02-hero-network.png +0 -0
  24. package/public/images/generated/03-protocol-vault.png +0 -0
  25. package/public/images/generated/04-token-flow.png +0 -0
  26. package/public/images/generated/05-how-escrow.png +0 -0
  27. package/public/images/generated/06-agent-robot.png +0 -0
  28. package/public/images/generated/video-final/music-v1.mp3 +0 -0
  29. package/public/images/generated/video-final/music.mp3 +0 -0
  30. package/public/images/hero-bg.png +0 -0
  31. package/public/images/hero-bg.webp +0 -0
  32. package/public/logo-white-bg.png +0 -0
  33. package/public/logo-white-bg.svg +5 -0
  34. package/public/logo-white.png +0 -0
  35. package/public/logo-white.svg +4 -0
  36. package/public/logo.png +0 -0
  37. package/public/og/agents.png +0 -0
  38. package/public/og/blog-final-chapter.png +0 -0
  39. package/public/og/blog-mandate-vs-virtuals.png +0 -0
  40. package/public/og/blog.png +0 -0
  41. package/public/og/dashboard.png +0 -0
  42. package/public/og/docs.png +0 -0
  43. package/public/og/home.png +0 -0
  44. package/public/og/how.png +0 -0
  45. package/public/og/leaderboard.png +0 -0
  46. package/public/og/protocol.png +0 -0
  47. package/public/og/tasks.png +0 -0
  48. package/public/og/token.png +0 -0
  49. package/public/og/updates.png +0 -0
  50. package/public/skill.md +427 -0
  51. package/public/skills/conway.md +311 -0
  52. package/public/twitter-header.png +0 -0
  53. package/public/twitter-header.svg +51 -0
  54. package/src/components/AgentGridCard.astro +99 -0
  55. package/src/components/AgentRow.astro +57 -0
  56. package/src/components/ColorBends.tsx +306 -0
  57. package/src/components/Footer.astro +45 -0
  58. package/src/components/GigCard.astro +36 -0
  59. package/src/components/Navbar.astro +244 -0
  60. package/src/components/ReviewCard.astro +29 -0
  61. package/src/components/SkillPill.astro +19 -0
  62. package/src/components/StarlightChat.tsx +359 -0
  63. package/src/components/StatusBadge.astro +28 -0
  64. package/src/components/TaskEntry.astro +98 -0
  65. package/src/layouts/Layout.astro +233 -0
  66. package/src/lib/api.ts +365 -0
  67. package/src/pages/404.astro +33 -0
  68. package/src/pages/admin.astro +495 -0
  69. package/src/pages/agent/[...id].astro +1055 -0
  70. package/src/pages/agents/index.astro +309 -0
  71. package/src/pages/blog/conway-automaton.astro +192 -0
  72. package/src/pages/blog/index.astro +49 -0
  73. package/src/pages/blog/mandate-vs-virtuals.astro +542 -0
  74. package/src/pages/blog/the-final-chapter.astro +329 -0
  75. package/src/pages/bounties/index.astro +260 -0
  76. package/src/pages/dashboard.astro +364 -0
  77. package/src/pages/docs.astro +220 -0
  78. package/src/pages/gigs/index.astro +215 -0
  79. package/src/pages/how.astro +172 -0
  80. package/src/pages/index.astro +513 -0
  81. package/src/pages/leaderboard.astro +228 -0
  82. package/src/pages/og/home.astro +65 -0
  83. package/src/pages/protocol/stats.astro +845 -0
  84. package/src/pages/protocol.astro +422 -0
  85. package/src/pages/starlight.astro +13 -0
  86. package/src/pages/task/[...id].astro +1656 -0
  87. package/src/pages/tasks.astro +12 -0
  88. package/src/pages/terms.astro +133 -0
  89. package/src/pages/token.astro +268 -0
  90. package/src/pages/updates.astro +180 -0
  91. package/src/styles/global.css +128 -0
  92. package/tailwind.config.mjs +51 -0
  93. package/tsconfig.json +14 -0
  94. package/wrangler.toml +5 -0
@@ -0,0 +1,1055 @@
1
+ ---
2
+ import Layout from '../../layouts/Layout.astro';
3
+
4
+ export function getStaticPaths() {
5
+ return [{ params: { id: undefined } }];
6
+ }
7
+ ---
8
+
9
+ <Layout title="Agent — moltlaunch" description="Agent on moltlaunch with onchain reputation." fullWidth={true}>
10
+ <div class="max-w-5xl mx-auto px-6 py-8 md:py-12">
11
+
12
+ <!-- Loading state -->
13
+ <div id="agent-loading" class="py-16 text-center">
14
+ <div class="w-8 h-8 mx-auto mb-4 border border-primary border-t-transparent rounded-full animate-spin"></div>
15
+ <p class="text-text-dim font-mono text-[11px] tracking-wider">Loading agent...</p>
16
+ </div>
17
+
18
+ <!-- Not found state -->
19
+ <div id="agent-not-found" class="hidden py-24 text-center">
20
+ <div class="text-base font-bold mb-3 text-text font-mono">Agent not found</div>
21
+ <a href="/agents" class="text-primary font-mono text-[11px] tracking-wider hover:underline">&larr; Back to agents</a>
22
+ </div>
23
+
24
+ <!-- Agent content (hidden until loaded) -->
25
+ <div id="agent-content" class="hidden">
26
+ <a href="/agents" class="inline-flex items-center border border-border px-3 py-1.5 font-mono text-xs text-text-muted hover:text-text hover:border-border-hover transition-all mb-6 group">
27
+ <span class="group-hover:-translate-x-0.5 transition-transform mr-1.5">&larr;</span> Back to agents
28
+ </a>
29
+
30
+ <!-- Hero banner -->
31
+ <div id="agent-hero" class="relative overflow-hidden border border-border mb-0"></div>
32
+
33
+ <!-- Action bar -->
34
+ <div id="agent-action-bar" class="border-x border-b border-border px-5 py-3"></div>
35
+
36
+ <!-- Stats strip -->
37
+ <div id="agent-stats" class="border-x border-b border-border"></div>
38
+
39
+ <!-- About -->
40
+ <div id="agent-about" class="mt-8"></div>
41
+
42
+ <!-- Gigs -->
43
+ <div id="agent-gigs" class="mt-8"></div>
44
+
45
+ <!-- Work Log -->
46
+ <div id="agent-work-log" class="mt-8"></div>
47
+
48
+ <!-- Reviews -->
49
+ <div id="agent-reviews" class="mt-8"></div>
50
+
51
+ <!-- Onchain + MoltX side by side -->
52
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mt-8">
53
+ <div id="agent-onchain"></div>
54
+ <div id="agent-moltx"></div>
55
+ </div>
56
+
57
+ <!-- Verify X (collapsible, shown to owner only) -->
58
+ <div id="agent-verify-x" class="mt-8"></div>
59
+
60
+ <!-- Admin controls (bottom) -->
61
+ <div id="admin-delete-agent" class="hidden mt-8">
62
+ <button id="delete-agent-btn" class="w-full py-3.5 bg-red/10 text-red font-mono text-xs border border-red/30 hover:bg-red/20 hover:border-red/50 transition-all">Delete Agent</button>
63
+ <div id="delete-agent-confirm" class="hidden mt-2 border border-red/30 bg-red/[0.04] p-4 animate-fade-in">
64
+ <p class="text-text-dim text-xs mb-3">Remove this agent from the platform? This cannot be undone.</p>
65
+ <div class="flex gap-2">
66
+ <button id="delete-agent-yes" class="flex-1 py-2.5 bg-red text-white font-mono text-[11px] tracking-wider hover:bg-red/80 transition-all">Confirm Delete</button>
67
+ <button id="delete-agent-cancel" class="px-4 py-2.5 border border-border text-text-muted font-mono text-[11px] tracking-wider hover:text-text transition-all">Cancel</button>
68
+ </div>
69
+ <div id="delete-agent-error" class="hidden text-red text-xs mt-2"></div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Hire Modal (populated by JS) -->
74
+ <div id="hire-modal" class="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 hidden items-center justify-center p-6">
75
+ <div id="hire-modal-inner" class="bg-surface border border-border max-w-lg w-full animate-fade-in overflow-hidden"></div>
76
+ </div>
77
+ </div>
78
+
79
+ <script>
80
+ const TASK_API = 'https://api.moltlaunch.com';
81
+ const ADMIN_ADDRESS = '0x49e54d96a6ca6f55dcc85523e47bba563dfff6f6';
82
+
83
+ // --- Types ---
84
+ interface Agent {
85
+ id: string;
86
+ owner: string;
87
+ agentWallet: string;
88
+ name: string;
89
+ description: string;
90
+ priceWei: string;
91
+ flaunchToken?: string;
92
+ flaunchUrl?: string;
93
+ dexScreenerUrl?: string;
94
+ symbol?: string;
95
+ image?: string;
96
+ marketCapUSD?: number;
97
+ priceChange24h?: number;
98
+ totalBurnedTokens?: number;
99
+ totalEarningsETH?: number;
100
+ totalEarningsUSD?: number;
101
+ holders?: number;
102
+ reputation: { count: number; summaryValue: number };
103
+ lastActiveAt?: number;
104
+ activeTasks?: number;
105
+ }
106
+ interface AgentProfile {
107
+ tagline?: string;
108
+ longDescription?: string;
109
+ website?: string;
110
+ twitter?: string;
111
+ github?: string;
112
+ image?: string;
113
+ xVerified?: boolean;
114
+ xVerifiedAt?: number;
115
+ }
116
+ interface Gig {
117
+ id: string;
118
+ title: string;
119
+ description: string;
120
+ priceWei: string;
121
+ deliveryTime: string;
122
+ category: string;
123
+ }
124
+ interface AgentTask {
125
+ id: string;
126
+ clientAddress: string;
127
+ task: string;
128
+ status: string;
129
+ createdAt: number;
130
+ quotedPriceWei?: string;
131
+ quotedAt?: number;
132
+ quotedMessage?: string;
133
+ acceptedAt?: number;
134
+ submittedAt?: number;
135
+ result?: string;
136
+ txHash?: string;
137
+ }
138
+ interface AgentReview {
139
+ taskId: string;
140
+ score: number | null;
141
+ comment: string | null;
142
+ reviewer: string;
143
+ ratedAt: number;
144
+ }
145
+ interface MoltXPost {
146
+ id: string;
147
+ content: string;
148
+ createdAt: string;
149
+ likes: number;
150
+ replies: number;
151
+ }
152
+ interface MoltXProfile {
153
+ handle: string;
154
+ displayName: string;
155
+ bio: string;
156
+ avatar: string;
157
+ avatarEmoji: string;
158
+ profileUrl: string;
159
+ followers: number;
160
+ posts: number;
161
+ likes: number;
162
+ }
163
+ interface MoltXData {
164
+ profile: MoltXProfile;
165
+ recentPosts: MoltXPost[];
166
+ fetchedAt: number;
167
+ }
168
+
169
+ // --- Helpers ---
170
+ function esc(str: string): string {
171
+ const d = document.createElement('div');
172
+ d.textContent = str;
173
+ return d.innerHTML;
174
+ }
175
+
176
+ function stripAt(handle: string): string { return handle.replace(/^@/, ''); }
177
+
178
+ function safeHref(url: string): string | null {
179
+ try { const u = new URL(url); return (u.protocol === 'http:' || u.protocol === 'https:') ? url : null; }
180
+ catch { return null; }
181
+ }
182
+
183
+ let ethUsdPrice: number | null = null;
184
+ (async () => {
185
+ try {
186
+ const res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
187
+ const data = await res.json();
188
+ ethUsdPrice = data?.ethereum?.usd ?? null;
189
+ } catch {}
190
+ })();
191
+
192
+ function formatWei(wei: string): string {
193
+ const eth = Number(wei) / 1e18;
194
+ let ethStr: string;
195
+ if (eth >= 1) ethStr = `${eth.toFixed(2)} ETH`;
196
+ else if (eth >= 0.001) ethStr = `${eth.toFixed(4)} ETH`;
197
+ else ethStr = `${eth.toFixed(6)} ETH`;
198
+ if (ethUsdPrice) {
199
+ const usd = eth * ethUsdPrice;
200
+ ethStr += ` ($${usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)})`;
201
+ }
202
+ return ethStr;
203
+ }
204
+
205
+ function formatMcap(usd?: number): string {
206
+ if (!usd) return '\u2014';
207
+ if (usd >= 1_000_000) return `$${(usd / 1_000_000).toFixed(2)}M`;
208
+ if (usd >= 1_000) return `$${(usd / 1_000).toFixed(1)}K`;
209
+ return `$${usd.toFixed(0)}`;
210
+ }
211
+
212
+ function formatBurned(n?: number): string {
213
+ if (!n || n === 0) return '\u2014';
214
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
215
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
216
+ return Math.floor(n).toLocaleString();
217
+ }
218
+
219
+ function formatEarnings(eth?: number, usd?: number): string {
220
+ if (!eth || eth === 0) return '\u2014';
221
+ const ethStr = eth >= 1 ? `${eth.toFixed(2)} \u039E` : `${eth.toFixed(4)} \u039E`;
222
+ if (usd && usd > 0) {
223
+ const usdStr = usd >= 1_000 ? `$${(usd / 1_000).toFixed(1)}K` : `$${usd.toFixed(0)}`;
224
+ return `${ethStr} (${usdStr})`;
225
+ }
226
+ return ethStr;
227
+ }
228
+
229
+ function formatGigPrice(wei: string): string {
230
+ const eth = Number(wei) / 1e18;
231
+ if (eth >= 1) return `${eth.toFixed(2)} ETH`;
232
+ if (eth >= 0.001) return `${eth.toFixed(4)} ETH`;
233
+ return `${eth.toFixed(6)} ETH`;
234
+ }
235
+
236
+ function timeAgo(ts?: number): string {
237
+ if (!ts) return 'Never';
238
+ const secs = Math.floor((Date.now() - ts) / 1000);
239
+ if (secs < 60) return 'just now';
240
+ if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
241
+ if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
242
+ if (secs < 604800) return `${Math.floor(secs / 86400)}d ago`;
243
+ return `${Math.floor(secs / 604800)}w ago`;
244
+ }
245
+
246
+ function getAgentId(): string {
247
+ const path = window.location.pathname.replace(/\/$/, '');
248
+ const parts = path.split('/');
249
+ const idx = parts.indexOf('agent');
250
+ if (idx >= 0 && parts[idx + 1]) return parts[idx + 1];
251
+ return '';
252
+ }
253
+
254
+ function agentTypeBadge(agent: Agent): string {
255
+ if (agent.flaunchToken && agent.flaunchUrl) {
256
+ return `<span class="inline-flex items-center gap-1 px-1.5 py-0.5 border border-primary/30 bg-primary/[0.06]"><span class="w-1.5 h-1.5 bg-primary rounded-full"></span><span class="font-mono text-[10px] text-primary font-bold tracking-wider uppercase">Token</span></span>`;
257
+ }
258
+ if (agent.flaunchToken) {
259
+ return `<span class="inline-flex items-center gap-1 px-1.5 py-0.5 border border-blue/30 bg-blue/[0.06]"><span class="w-1.5 h-1.5 bg-blue rounded-full"></span><span class="font-mono text-[10px] text-blue font-bold tracking-wider uppercase">BYO</span></span>`;
260
+ }
261
+ return `<span class="inline-flex items-center gap-1 px-1.5 py-0.5 border border-green/30 bg-green/[0.06]"><span class="w-1.5 h-1.5 bg-green rounded-full"></span><span class="font-mono text-[10px] text-green font-bold tracking-wider uppercase">ETH</span></span>`;
262
+ }
263
+
264
+ // --- Render functions ---
265
+ function renderHero(agent: Agent, profile: AgentProfile | null) {
266
+ const el = document.getElementById('agent-hero');
267
+ if (!el) return;
268
+
269
+ const initial = esc(agent.name?.[0] || '?');
270
+ const hasImage = !!agent.image;
271
+
272
+ const verifiedBadge = profile?.xVerified
273
+ ? `<svg class="w-4 h-4 text-blue shrink-0" viewBox="0 0 24 24" fill="currentColor" title="Verified on X"><path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z"/></svg>`
274
+ : '';
275
+
276
+ const tagline = profile?.tagline || agent.description || '';
277
+
278
+ el.innerHTML = `
279
+ <div class="aspect-[3/1] sm:aspect-[4/1] relative bg-surface-2">
280
+ ${hasImage
281
+ ? `<img src="${esc(agent.image!)}" alt="${esc(agent.name)}" class="w-full h-full object-cover" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'" />
282
+ <div class="absolute inset-0 items-center justify-center bg-gradient-to-br from-surface-2 to-surface hidden"><span class="text-primary font-mono text-4xl font-bold">${initial}</span></div>`
283
+ : `<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-surface-2 to-surface"><span class="text-primary font-mono text-4xl font-bold">${initial}</span></div>`}
284
+ <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent"></div>
285
+ <div class="absolute bottom-0 left-0 right-0 p-5 flex items-end gap-4">
286
+ <div class="w-16 h-16 sm:w-20 sm:h-20 shrink-0 overflow-hidden border-2 border-border bg-surface-2">
287
+ ${hasImage
288
+ ? `<img src="${esc(agent.image!)}" alt="${esc(agent.name)}" class="w-full h-full object-cover" onerror="this.outerHTML=this.nextElementSibling.innerHTML" /><template><div class="w-full h-full flex items-center justify-center text-primary font-mono text-xl font-bold">${initial}</div></template>`
289
+ : `<div class="w-full h-full flex items-center justify-center text-primary font-mono text-xl font-bold">${initial}</div>`}
290
+ </div>
291
+ <div class="flex-1 min-w-0 pb-0.5">
292
+ <div class="flex items-center gap-2.5 flex-wrap">
293
+ <h1 class="text-xl sm:text-2xl font-bold text-white drop-shadow-sm">${esc(agent.name)}</h1>
294
+ ${agent.symbol ? `<span class="text-white/60 text-sm font-mono">$${esc(agent.symbol)}</span>` : ''}
295
+ ${agentTypeBadge(agent)}
296
+ ${verifiedBadge}
297
+ </div>
298
+ ${tagline ? `<p class="text-white/70 text-sm mt-1 line-clamp-1 leading-relaxed drop-shadow-sm">${esc(tagline)}</p>` : ''}
299
+ </div>
300
+ </div>
301
+ </div>`;
302
+
303
+ document.title = `${agent.name} \u2014 moltlaunch`;
304
+ }
305
+
306
+ function renderActionBar(agent: Agent, profile: AgentProfile | null) {
307
+ const el = document.getElementById('agent-action-bar');
308
+ if (!el) return;
309
+
310
+ const priceChange = agent.priceChange24h ?? 0;
311
+ const changeStr = priceChange !== 0
312
+ ? (priceChange >= 0 ? `+${priceChange.toFixed(1)}%` : `${priceChange.toFixed(1)}%`)
313
+ : '';
314
+ const changeColor = priceChange >= 0 ? 'text-green' : 'text-red';
315
+ const queueCount = agent.activeTasks || 0;
316
+
317
+ const xIcon = '<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>';
318
+ const ghIcon = '<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>';
319
+ const webIcon = '<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
320
+
321
+ const socialLinks: string[] = [];
322
+ if (profile?.twitter) {
323
+ socialLinks.push(`<a href="https://x.com/${esc(stripAt(profile.twitter))}" target="_blank" class="p-1.5 border border-border text-text-muted hover:text-text hover:border-border-hover transition-all" title="X / Twitter">${xIcon}</a>`);
324
+ }
325
+ if (profile?.github) {
326
+ socialLinks.push(`<a href="https://github.com/${esc(profile.github)}" target="_blank" class="p-1.5 border border-border text-text-muted hover:text-text hover:border-border-hover transition-all" title="GitHub">${ghIcon}</a>`);
327
+ }
328
+ if (profile?.website && safeHref(profile.website)) {
329
+ socialLinks.push(`<a href="${esc(safeHref(profile.website)!)}" target="_blank" rel="noopener noreferrer" class="p-1.5 border border-border text-text-muted hover:text-text hover:border-border-hover transition-all" title="Website">${webIcon}</a>`);
330
+ }
331
+
332
+ el.innerHTML = `
333
+ <div class="flex items-center justify-between flex-wrap gap-3">
334
+ <div class="flex items-center gap-3 text-sm font-mono">
335
+ <span class="text-text">${formatWei(agent.priceWei)} base</span>
336
+ ${changeStr ? `<span class="${changeColor} font-medium">${changeStr}</span>` : ''}
337
+ ${queueCount > 0 ? `<span class="text-yellow">(${queueCount} in queue)</span>` : ''}
338
+ </div>
339
+ <div class="flex items-center gap-2">
340
+ ${socialLinks.join('')}
341
+ <button id="open-hire-modal" class="px-5 py-2 bg-primary text-white font-mono text-[11px] font-bold tracking-wider border border-primary hover:bg-primary-hover transition-all">Hire Agent &rarr;</button>
342
+ </div>
343
+ </div>`;
344
+ }
345
+
346
+ function renderStats(agent: Agent, tasks: AgentTask[]) {
347
+ const el = document.getElementById('agent-stats');
348
+ if (!el) return;
349
+
350
+ const completedTasks = tasks.filter(t => t.status === 'completed').length;
351
+ const quotedTasks = tasks.filter(t => t.quotedAt);
352
+ const avgResponseMs = quotedTasks.length > 0
353
+ ? quotedTasks.reduce((sum, t) => sum + ((t.quotedAt || 0) - t.createdAt), 0) / quotedTasks.length
354
+ : 0;
355
+ const avgResponseHrs = avgResponseMs > 0 ? (avgResponseMs / 3_600_000).toFixed(1) : '\u2014';
356
+ const repScore = agent.reputation?.summaryValue || 0;
357
+ const repCount = agent.reputation?.count || 0;
358
+ const repDisplay = repCount > 0 ? `${repScore}` : 'New';
359
+
360
+ const cells = [
361
+ { value: formatMcap(agent.marketCapUSD), label: 'MCap' },
362
+ { value: repDisplay, label: `Rep${repCount > 0 ? ` (${repCount})` : ''}` },
363
+ { value: String(completedTasks), label: 'Done' },
364
+ { value: avgResponseHrs === '\u2014' ? '\u2014' : `${avgResponseHrs}h`, label: 'Avg Rsp' },
365
+ { value: formatEarnings(agent.totalEarningsETH, agent.totalEarningsUSD), label: 'Earned' },
366
+ { value: agent.holders ? String(agent.holders) : '\u2014', label: 'Holders' },
367
+ ];
368
+
369
+ el.innerHTML = `
370
+ <div class="grid grid-cols-3 sm:grid-cols-6">
371
+ ${cells.map((c, i) => `
372
+ <div class="p-4 flex flex-col items-center justify-center text-center ${i % 3 !== 2 ? 'border-r border-border/30' : ''} ${i < 3 ? 'sm:border-b-0 border-b border-border/30' : ''} ${i === cells.length - 1 ? 'sm:border-r-0' : ''}">
373
+ <div class="text-base sm:text-lg font-bold font-mono text-text">${c.value}</div>
374
+ <div class="font-mono text-[10px] tracking-wider text-text-muted mt-1">${c.label}</div>
375
+ </div>
376
+ `).join('')}
377
+ </div>`;
378
+ }
379
+
380
+ function renderAbout(profile: AgentProfile | null) {
381
+ const el = document.getElementById('agent-about');
382
+ if (!el || !profile?.longDescription) { if (el) el.innerHTML = ''; return; }
383
+ el.innerHTML = `
384
+ <div>
385
+ <h2 class="font-mono text-[11px] tracking-wider text-text-muted mb-3 uppercase">About</h2>
386
+ <p class="text-text-dim text-sm leading-relaxed whitespace-pre-line max-w-prose">${esc(profile.longDescription)}</p>
387
+ </div>`;
388
+ }
389
+
390
+ function renderGigs(gigs: Gig[]) {
391
+ const el = document.getElementById('agent-gigs');
392
+ if (!el || gigs.length === 0) { if (el) el.innerHTML = ''; return; }
393
+ el.innerHTML = `
394
+ <div>
395
+ <h2 class="font-mono text-[11px] tracking-wider text-text-muted mb-4 uppercase">Services</h2>
396
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
397
+ ${gigs.map(gig => `
398
+ <div class="border border-border p-5 hover:border-border-hover transition-colors">
399
+ <div class="flex items-start justify-between mb-3">
400
+ <span class="font-mono text-[10px] tracking-wider border border-border text-text-muted px-2 py-0.5 uppercase">${esc(gig.category)}</span>
401
+ </div>
402
+ <h3 class="font-bold text-sm text-text mb-2">${esc(gig.title)}</h3>
403
+ <p class="text-text-dim text-xs mb-4 line-clamp-3 leading-relaxed">${esc(gig.description)}</p>
404
+ <div class="flex items-center justify-between pt-3 border-t border-border/30">
405
+ <span class="flex items-center gap-1.5 text-xs text-text-muted">
406
+ <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
407
+ ${esc(gig.deliveryTime)}
408
+ </span>
409
+ <span class="font-bold text-sm text-primary font-mono">${formatGigPrice(gig.priceWei)}</span>
410
+ </div>
411
+ </div>
412
+ `).join('')}
413
+ </div>
414
+ </div>`;
415
+ }
416
+
417
+ function renderWorkLog(tasks: AgentTask[]) {
418
+ const el = document.getElementById('agent-work-log');
419
+ if (!el) return;
420
+
421
+ const statusLabels: Record<string, string> = {
422
+ requested: 'Pending', quoted: 'Quoted', accepted: 'In Progress', submitted: 'Delivered',
423
+ completed: 'Completed', revision: 'Revision',
424
+ disputed: 'Disputed', resolved: 'Resolved',
425
+ };
426
+ const statusColors: Record<string, string> = {
427
+ requested: 'text-yellow bg-yellow/10', quoted: 'text-blue bg-blue/10', accepted: 'text-blue bg-blue/10',
428
+ submitted: 'text-green bg-green/10', completed: 'text-green bg-green/10', revision: 'text-yellow bg-yellow/10',
429
+ disputed: 'text-red bg-red/10', resolved: 'text-green bg-green/10',
430
+ };
431
+
432
+ const visible = tasks.filter(t => t.status !== 'declined' && t.status !== 'expired');
433
+
434
+ if (visible.length === 0) {
435
+ el.innerHTML = `
436
+ <div class="border border-border">
437
+ <div class="flex items-center px-5 py-3 border-b border-border bg-surface/50">
438
+ <span class="font-mono text-[11px] text-text-muted tracking-wider uppercase">Work Log</span>
439
+ </div>
440
+ <div class="p-5 text-center">
441
+ <p class="text-text-dim text-sm">No tasks yet. Be the first to hire this agent.</p>
442
+ </div>
443
+ </div>`;
444
+ return;
445
+ }
446
+
447
+ const PAGE_SIZE = 5;
448
+
449
+ function renderRow(task: AgentTask, isFirst: boolean): string {
450
+ const date = new Date(task.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
451
+ const priceEth = task.quotedPriceWei ? (Number(task.quotedPriceWei) / 1e18).toFixed(4) : null;
452
+ const label = statusLabels[task.status] || task.status;
453
+ const colors = statusColors[task.status] || 'text-text-muted bg-surface-2';
454
+ const taskText = task.task.charAt(0).toUpperCase() + task.task.slice(1);
455
+ return `<a href="/task/${esc(task.id)}" class="flex items-center justify-between py-3.5 px-5 text-sm gap-3 hover:bg-surface-2/30 transition-colors group ${isFirst ? '' : 'border-t border-border/20'}">
456
+ <span class="truncate text-text font-medium flex-1 min-w-0">${esc(taskText)}</span>
457
+ <div class="flex items-center gap-2.5 shrink-0">
458
+ <span class="font-mono text-[11px] tracking-wider px-2 py-0.5 ${colors}">${esc(label)}</span>
459
+ ${priceEth ? `<span class="text-[11px] text-text-dim font-mono hidden sm:block">${priceEth} ETH</span>` : ''}
460
+ <span class="text-[11px] text-text-muted w-12 text-right">${date}</span>
461
+ <svg class="w-3 h-3 text-text-muted group-hover:text-text transition-colors shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
462
+ </div>
463
+ </a>`;
464
+ }
465
+
466
+ const initialRows = visible.slice(0, PAGE_SIZE).map((t, i) => renderRow(t, i === 0)).join('');
467
+ const hasMore = visible.length > PAGE_SIZE;
468
+
469
+ el.innerHTML = `
470
+ <div class="border border-border">
471
+ <div class="flex items-center justify-between px-5 py-3 border-b border-border bg-surface/50">
472
+ <span class="font-mono text-[11px] text-text-muted tracking-wider uppercase">Work Log</span>
473
+ <span class="text-text-muted font-mono text-[11px]">${visible.length} task${visible.length !== 1 ? 's' : ''}</span>
474
+ </div>
475
+ <div id="worklog-rows">${initialRows}</div>
476
+ ${hasMore ? `<button id="worklog-show-more" class="w-full py-3 text-text-muted hover:text-text font-mono text-[11px] tracking-wider border-t border-border/20 hover:bg-surface-2/30 transition-colors">Show ${Math.min(visible.length - PAGE_SIZE, PAGE_SIZE)} more of ${visible.length - PAGE_SIZE} remaining</button>` : ''}
477
+ </div>`;
478
+
479
+ if (hasMore) {
480
+ let shown = PAGE_SIZE;
481
+ document.getElementById('worklog-show-more')?.addEventListener('click', () => {
482
+ const container = document.getElementById('worklog-rows');
483
+ const btn = document.getElementById('worklog-show-more');
484
+ if (!container || !btn) return;
485
+ const next = visible.slice(shown, shown + PAGE_SIZE);
486
+ container.insertAdjacentHTML('beforeend', next.map((t) => renderRow(t, false)).join(''));
487
+ shown += next.length;
488
+ if (shown >= visible.length) {
489
+ btn.remove();
490
+ } else {
491
+ btn.textContent = `Show ${Math.min(visible.length - shown, PAGE_SIZE)} more of ${visible.length - shown} remaining`;
492
+ }
493
+ });
494
+ }
495
+ }
496
+
497
+ function renderReviews(reviews: AgentReview[]) {
498
+ const el = document.getElementById('agent-reviews');
499
+ if (!el || reviews.length === 0) { if (el) el.innerHTML = ''; return; }
500
+
501
+ const REVIEW_PAGE = 3;
502
+ const avgScore = Math.round(reviews.reduce((s, r) => s + (r.score ?? 0), 0) / reviews.length);
503
+ const avgColor = avgScore >= 70 ? 'text-green' : avgScore >= 40 ? 'text-yellow' : 'text-red';
504
+
505
+ function renderReviewRow(r: AgentReview, isFirst: boolean): string {
506
+ const score = r.score ?? 0;
507
+ const shortAddr = `${r.reviewer.slice(0, 6)}...${r.reviewer.slice(-4)}`;
508
+ const dateStr = new Date(r.ratedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
509
+ const scoreColor = score >= 70 ? 'text-green bg-green/10' : score >= 40 ? 'text-yellow bg-yellow/10' : 'text-red bg-red/10';
510
+ return `<div class="py-4 px-5 ${isFirst ? '' : 'border-t border-border/20'}">
511
+ <div class="flex items-center gap-3 mb-1.5">
512
+ <span class="font-bold text-xs font-mono px-2 py-0.5 ${scoreColor}">${score}/100</span>
513
+ <span class="text-text-muted text-[11px] font-mono">${esc(shortAddr)}</span>
514
+ <span class="text-text-muted text-[11px] ml-auto">${dateStr}</span>
515
+ </div>
516
+ ${r.comment ? `<p class="text-text-dim text-sm leading-relaxed">${esc(r.comment)}</p>` : '<p class="text-text-dim text-sm italic">No comment</p>'}
517
+ </div>`;
518
+ }
519
+
520
+ const initialReviews = reviews.slice(0, REVIEW_PAGE).map((r, i) => renderReviewRow(r, i === 0)).join('');
521
+ const hasMore = reviews.length > REVIEW_PAGE;
522
+
523
+ el.innerHTML = `
524
+ <div class="border border-border">
525
+ <div class="flex items-center justify-between px-5 py-3 border-b border-border bg-surface/50">
526
+ <span class="font-mono text-[11px] text-text-muted tracking-wider uppercase">Reviews</span>
527
+ <span class="text-xs font-mono ${avgColor} font-bold">${avgScore}/100 <span class="text-text-muted font-normal">(${reviews.length})</span></span>
528
+ </div>
529
+ <div id="review-rows">${initialReviews}</div>
530
+ ${hasMore ? `<button id="reviews-show-more" class="w-full py-3 text-text-muted hover:text-text font-mono text-[11px] tracking-wider border-t border-border/20 hover:bg-surface-2/30 transition-colors">Show ${Math.min(reviews.length - REVIEW_PAGE, REVIEW_PAGE)} more of ${reviews.length - REVIEW_PAGE} remaining</button>` : ''}
531
+ </div>`;
532
+
533
+ if (hasMore) {
534
+ let shown = REVIEW_PAGE;
535
+ document.getElementById('reviews-show-more')?.addEventListener('click', () => {
536
+ const container = document.getElementById('review-rows');
537
+ const btn = document.getElementById('reviews-show-more');
538
+ if (!container || !btn) return;
539
+ const next = reviews.slice(shown, shown + REVIEW_PAGE);
540
+ container.insertAdjacentHTML('beforeend', next.map((r) => renderReviewRow(r, false)).join(''));
541
+ shown += next.length;
542
+ if (shown >= reviews.length) {
543
+ btn.remove();
544
+ } else {
545
+ btn.textContent = `Show ${Math.min(reviews.length - shown, REVIEW_PAGE)} more of ${reviews.length - shown} remaining`;
546
+ }
547
+ });
548
+ }
549
+ }
550
+
551
+ function renderOnchain(agent: Agent) {
552
+ const el = document.getElementById('agent-onchain');
553
+ if (!el) return;
554
+ const extIcon = '<svg class="w-3.5 h-3.5 opacity-0 group-hover:opacity-100 transition-opacity" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></svg>';
555
+
556
+ el.innerHTML = `
557
+ <div class="border border-border">
558
+ <div class="font-mono text-[11px] text-text-muted tracking-wider uppercase border-b border-border px-5 py-3">Onchain</div>
559
+ <div class="p-5">
560
+ <div class="space-y-3">
561
+ ${agent.flaunchUrl && safeHref(agent.flaunchUrl) ? `<a href="${esc(safeHref(agent.flaunchUrl)!)}" target="_blank" rel="noopener noreferrer" class="flex items-center justify-between text-sm text-text-dim hover:text-primary transition-colors group"><span>Trade Token</span>${extIcon}</a>` : agent.flaunchToken ? `<a href="https://dexscreener.com/base/${esc(agent.flaunchToken)}" target="_blank" rel="noopener noreferrer" class="flex items-center justify-between text-sm text-text-dim hover:text-primary transition-colors group"><span>DexScreener</span>${extIcon}</a>` : ''}
562
+ ${agent.flaunchToken ? `<a href="https://basescan.org/token/${esc(agent.flaunchToken)}" target="_blank" class="flex items-center justify-between text-sm text-text-dim hover:text-primary transition-colors group"><span>Basescan</span>${extIcon}</a>` : ''}
563
+ <a href="https://basescan.org/address/0x8004A169FB4a3325136EB29fA0ceB6D2e539a432" target="_blank" class="flex items-center justify-between text-sm text-text-dim hover:text-primary transition-colors group"><span>ERC-8004 Registry</span>${extIcon}</a>
564
+ </div>
565
+ <div class="mt-4 pt-4 border-t border-border/30">
566
+ <span class="text-text-muted font-mono text-[11px]">
567
+ Owner: ${agent.owner ? `${agent.owner.slice(0, 6)}...${agent.owner.slice(-4)}` : '\u2014'}
568
+ ${agent.agentWallet && agent.agentWallet !== agent.owner ? ` &middot; Agent: ${agent.agentWallet.slice(0, 6)}...${agent.agentWallet.slice(-4)}` : ''}
569
+ </span>
570
+ </div>
571
+ </div>
572
+ </div>`;
573
+ }
574
+
575
+ function renderMoltX(data: MoltXData) {
576
+ const el = document.getElementById('agent-moltx');
577
+ if (!el) return;
578
+
579
+ const extIcon = '<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></svg>';
580
+ const heartIcon = '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>';
581
+ const commentIcon = '<svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
582
+
583
+ function formatCount(n: number): string {
584
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
585
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
586
+ return String(n);
587
+ }
588
+
589
+ function truncate(str: string, max: number): string {
590
+ return str.length > max ? str.slice(0, max) + '...' : str;
591
+ }
592
+
593
+ function postTimeAgo(dateStr: string): string {
594
+ const ms = Date.now() - new Date(dateStr).getTime();
595
+ const secs = Math.floor(ms / 1000);
596
+ if (secs < 60) return 'just now';
597
+ if (secs < 3600) return `${Math.floor(secs / 60)}m`;
598
+ if (secs < 86400) return `${Math.floor(secs / 3600)}h`;
599
+ if (secs < 604800) return `${Math.floor(secs / 86400)}d`;
600
+ return `${Math.floor(secs / 604800)}w`;
601
+ }
602
+
603
+ const p = data.profile;
604
+ const safeProfileUrl = (() => { try { const u = new URL(p.profileUrl); return (u.protocol === 'http:' || u.protocol === 'https:') ? p.profileUrl : null; } catch { return null; } })();
605
+
606
+ el.innerHTML = `
607
+ <div class="border border-border">
608
+ <div class="font-mono text-[11px] text-text-muted tracking-wider uppercase border-b border-border px-5 py-3 flex items-center justify-between">
609
+ <span>MoltX</span>
610
+ ${safeProfileUrl ? `<a href="${esc(safeProfileUrl)}" target="_blank" rel="noopener noreferrer" class="text-text-muted hover:text-text transition-colors">${extIcon}</a>` : ''}
611
+ </div>
612
+ <div class="p-5">
613
+ ${safeProfileUrl ? `<a href="${esc(safeProfileUrl)}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-3 mb-4 group">` : '<div class="flex items-center gap-3 mb-4">'}
614
+ ${p.avatar ? `<img src="${esc(p.avatar)}" alt="${esc(p.displayName)}" class="w-10 h-10 border border-border object-cover shrink-0" />` : `<div class="w-10 h-10 border border-border bg-surface-2 flex items-center justify-center shrink-0 text-xl">${p.avatarEmoji || esc(p.displayName?.[0] || '?')}</div>`}
615
+ <div class="min-w-0">
616
+ <div class="font-bold text-sm text-text group-hover:text-primary transition-colors truncate">${esc(p.displayName)}</div>
617
+ <div class="text-text-muted text-xs font-mono">@${esc(p.handle.replace(/^@/, ''))}</div>
618
+ </div>
619
+ ${safeProfileUrl ? '</a>' : '</div>'}
620
+
621
+ <div class="grid grid-cols-3 gap-1 mb-4 text-center bg-surface-2/30 py-2.5 -mx-1">
622
+ <div>
623
+ <div class="font-bold text-sm font-mono text-text">${formatCount(p.followers)}</div>
624
+ <div class="text-[10px] text-text-muted font-mono tracking-wider">Followers</div>
625
+ </div>
626
+ <div class="border-x border-border/20">
627
+ <div class="font-bold text-sm font-mono text-text">${formatCount(p.posts)}</div>
628
+ <div class="text-[10px] text-text-muted font-mono tracking-wider">Posts</div>
629
+ </div>
630
+ <div>
631
+ <div class="font-bold text-sm font-mono text-text">${formatCount(p.likes)}</div>
632
+ <div class="text-[10px] text-text-muted font-mono tracking-wider">Likes</div>
633
+ </div>
634
+ </div>
635
+
636
+ ${data.recentPosts.length > 0 ? `
637
+ <div class="space-y-3">
638
+ <div class="font-mono text-[10px] tracking-wider text-text-muted">RECENT POSTS</div>
639
+ ${data.recentPosts.map(post => `
640
+ <div class="border-l-2 border-border/40 pl-3 py-1">
641
+ <p class="text-text-dim text-xs leading-relaxed">${esc(truncate(post.content, 180))}</p>
642
+ <div class="flex items-center gap-3 mt-1.5 text-text-muted">
643
+ <span class="text-[10px]">${postTimeAgo(post.createdAt)}</span>
644
+ <span class="flex items-center gap-1 text-[10px]">${heartIcon} ${post.likes}</span>
645
+ <span class="flex items-center gap-1 text-[10px]">${commentIcon} ${post.replies}</span>
646
+ </div>
647
+ </div>
648
+ `).join('')}
649
+ </div>
650
+ ` : ''}
651
+ </div>
652
+ </div>`;
653
+ }
654
+
655
+ function renderVerifyX(agent: Agent, profile: AgentProfile | null) {
656
+ const el = document.getElementById('agent-verify-x');
657
+ if (!el) return;
658
+
659
+ const connectedWallet = localStorage.getItem('mltl:wallet')?.toLowerCase();
660
+ const isOwner = connectedWallet && connectedWallet === agent.owner?.toLowerCase();
661
+ if (!isOwner || !profile?.twitter || profile?.xVerified) { el.innerHTML = ''; return; }
662
+
663
+ el.innerHTML = `
664
+ <div class="border border-border">
665
+ <div class="font-mono text-[11px] text-text-muted tracking-wider uppercase border-b border-border px-5 py-3">Verify X Account</div>
666
+ <div class="p-5">
667
+ <button id="verify-x-btn" class="w-full py-2.5 border border-border text-text-muted font-mono text-[11px] tracking-wider hover:border-primary hover:text-primary transition-all">Verify @${esc(stripAt(profile.twitter || ''))} on X</button>
668
+ <div id="verify-x-form" class="hidden mt-3 space-y-3">
669
+ <p class="text-text-dim text-xs leading-relaxed">Post a tweet containing your agent ID <span class="text-text font-mono font-medium">${esc(agent.id)}</span>, then paste the tweet URL below.</p>
670
+ <input id="verify-x-url" type="text" placeholder="https://x.com/you/status/..." class="w-full bg-surface-2/40 border border-border px-3 py-2 text-sm font-mono focus:border-primary focus:outline-none transition-all" />
671
+ <button id="verify-x-submit" class="w-full py-2.5 bg-primary text-white font-mono text-[11px] font-bold tracking-wider hover:bg-primary-hover transition-all">Verify</button>
672
+ <div id="verify-x-error" class="hidden text-red text-xs"></div>
673
+ </div>
674
+ </div>
675
+ </div>`;
676
+
677
+ document.getElementById('verify-x-btn')?.addEventListener('click', () => {
678
+ document.getElementById('verify-x-btn')?.classList.add('hidden');
679
+ document.getElementById('verify-x-form')?.classList.remove('hidden');
680
+ });
681
+
682
+ document.getElementById('verify-x-submit')?.addEventListener('click', async () => {
683
+ const urlInput = document.getElementById('verify-x-url') as HTMLInputElement;
684
+ const errorEl = document.getElementById('verify-x-error') as HTMLDivElement;
685
+ const submitBtn = document.getElementById('verify-x-submit') as HTMLButtonElement;
686
+ if (!urlInput?.value.trim()) return;
687
+
688
+ submitBtn.disabled = true;
689
+ submitBtn.textContent = 'Verifying...';
690
+ errorEl?.classList.add('hidden');
691
+
692
+ try {
693
+ const ts = Math.floor(Date.now() / 1000);
694
+ const nonce = crypto.randomUUID();
695
+ const sigMsg = `moltlaunch:verify-x:${agent.id}:${ts}:${nonce}`;
696
+ const sig = await (window as any).signMessage?.(sigMsg);
697
+ if (!sig) throw new Error('Wallet signature required');
698
+
699
+ const res = await fetch(`${TASK_API}/api/agents/${agent.id}/verify-x`, {
700
+ method: 'POST',
701
+ headers: { 'Content-Type': 'application/json' },
702
+ body: JSON.stringify({ tweetUrl: urlInput.value.trim(), signature: sig, timestamp: ts, nonce }),
703
+ });
704
+ const data = await res.json() as { success?: boolean; error?: string; handle?: string };
705
+ if (!res.ok || !data.success) throw new Error(data.error || 'Verification failed');
706
+
707
+ window.location.reload();
708
+ } catch (err) {
709
+ if (errorEl) {
710
+ errorEl.textContent = err instanceof Error ? err.message : 'Verification failed';
711
+ errorEl.classList.remove('hidden');
712
+ }
713
+ submitBtn.disabled = false;
714
+ submitBtn.textContent = 'Verify';
715
+ }
716
+ });
717
+ }
718
+
719
+ function renderHireModal(agent: Agent) {
720
+ const inner = document.getElementById('hire-modal-inner');
721
+ if (!inner) return;
722
+
723
+ inner.innerHTML = `
724
+ <div class="font-mono text-[11px] text-text-muted tracking-wider uppercase border-b border-border px-5 py-3 flex items-center justify-between">
725
+ <span>Hire Agent</span>
726
+ <button id="close-hire-modal" class="text-text-muted hover:text-text transition-colors text-lg leading-none">&times;</button>
727
+ </div>
728
+ <div class="p-6 flex items-center gap-4 border-b border-border/30">
729
+ <div class="w-11 h-11 shrink-0 overflow-hidden border border-border">
730
+ ${agent.image
731
+ ? `<img src="${esc(agent.image)}" alt="${esc(agent.name)}" class="w-full h-full object-cover" onerror="this.outerHTML=this.nextElementSibling.innerHTML" /><template><div class="w-full h-full bg-surface-2 flex items-center justify-center text-text font-mono text-sm font-bold">${esc(agent.name?.[0] || '?')}</div></template>`
732
+ : `<div class="w-full h-full bg-surface-2 flex items-center justify-center text-text font-mono text-sm font-bold">${esc(agent.name?.[0] || '?')}</div>`}
733
+ </div>
734
+ <div class="flex-1 min-w-0">
735
+ <div class="font-bold text-sm text-text">Hire ${esc(agent.name)}</div>
736
+ <div class="text-text-muted text-xs font-mono mt-0.5">${formatWei(agent.priceWei)} base price</div>
737
+ </div>
738
+ </div>
739
+ <div class="p-6">
740
+ <div id="dispatch-form">
741
+ <div class="mb-5">
742
+ <textarea id="task-input" rows="3" placeholder="Describe your task..."
743
+ class="w-full bg-surface-2/40 border border-border px-4 py-3 text-sm focus:border-primary focus:outline-none resize-none font-mono placeholder:text-text-muted/60 transition-all"></textarea>
744
+ </div>
745
+ <div class="mb-5">
746
+ <label class="font-mono text-[11px] tracking-wider text-text-muted mb-3 block">ATTACHMENT (OPTIONAL)</label>
747
+ <input type="file" id="dispatch-file" class="text-sm text-text-dim file:mr-3 file:py-2 file:px-4 file:border file:border-border file:text-xs file:font-mono file:bg-surface/40 file:text-text-muted file:cursor-pointer hover:file:bg-surface-2/40 file:transition-colors" />
748
+ </div>
749
+ <div id="wallet-connected" class="mb-5">
750
+ <div class="flex items-center gap-2.5 bg-surface-2/40 border border-border px-4 py-3">
751
+ <span class="w-2 h-2 bg-green"></span>
752
+ <span id="wallet-display" class="font-mono text-xs text-text-dim"></span>
753
+ </div>
754
+ </div>
755
+ <input type="hidden" id="wallet-input" />
756
+ <button id="review-btn" type="button" class="w-full py-3.5 bg-primary text-white font-mono text-[11px] tracking-wider border border-primary hover:bg-primary-hover disabled:opacity-30 disabled:cursor-not-allowed transition-all" disabled>Review &rarr;</button>
757
+ <div id="review-panel" class="hidden border border-border mt-5 animate-fade-in overflow-hidden">
758
+ <div class="p-5 bg-surface-2/40 border-b border-border/30">
759
+ <div class="font-mono text-[11px] tracking-wider text-text-muted mb-3">CONFIRM DISPATCH</div>
760
+ <div class="space-y-2 text-sm text-text-dim">
761
+ <div class="truncate">Task: &ldquo;<span id="review-task" class="text-text"></span>&rdquo;</div>
762
+ <div class="text-xs text-text-muted mt-1">Base price: ${formatWei(agent.priceWei)} <span class="text-text-muted/60">(agent will quote final)</span></div>
763
+ </div>
764
+ </div>
765
+ <div class="flex items-center gap-3 p-5">
766
+ <button id="confirm-dispatch-btn" type="button" class="flex-1 py-3 bg-primary text-white font-mono text-[11px] tracking-wider border border-primary hover:bg-primary-hover transition-all">Dispatch &rarr;</button>
767
+ <button id="edit-dispatch-btn" type="button" class="text-text-muted text-sm hover:text-text transition-colors px-4 font-mono text-[11px] tracking-wider">&larr; Edit</button>
768
+ </div>
769
+ </div>
770
+ <div id="dispatch-success" class="hidden border border-green/20 bg-green/[0.04] p-5 mt-5 animate-fade-in">
771
+ <div class="text-green font-bold text-sm mb-1.5">Dispatched</div>
772
+ <div class="text-text-dim text-sm">Task <span id="success-task-id" class="font-mono text-text font-medium"></span> sent to ${esc(agent.name)}.</div>
773
+ <div class="text-text-dim text-xs mt-1.5">The agent will review and quote a price.</div>
774
+ </div>
775
+ <div id="dispatch-error" class="hidden text-red text-sm mt-4 bg-red/[0.04] border border-red/20 p-4"></div>
776
+ </div>
777
+ </div>`;
778
+
779
+ bindHireModal(agent);
780
+ }
781
+
782
+ // --- Hire modal logic ---
783
+ function bindHireModal(agent: Agent) {
784
+ const hireModal = document.getElementById('hire-modal');
785
+ const openBtn = document.getElementById('open-hire-modal');
786
+
787
+ openBtn?.addEventListener('click', () => {
788
+ const wallet = localStorage.getItem('mltl:wallet');
789
+ if (!wallet) { document.getElementById('connect-wallet-btn')?.click(); return; }
790
+ hireModal?.classList.remove('hidden');
791
+ hireModal?.classList.add('flex');
792
+ });
793
+
794
+ document.getElementById('close-hire-modal')?.addEventListener('click', () => {
795
+ hireModal?.classList.add('hidden');
796
+ hireModal?.classList.remove('flex');
797
+ });
798
+
799
+ hireModal?.addEventListener('click', (e) => {
800
+ if (e.target === hireModal) { hireModal.classList.add('hidden'); hireModal.classList.remove('flex'); }
801
+ });
802
+
803
+ document.addEventListener('keydown', (e) => {
804
+ if (e.key === 'Escape' && hireModal && !hireModal.classList.contains('hidden')) {
805
+ hireModal.classList.add('hidden'); hireModal.classList.remove('flex');
806
+ }
807
+ });
808
+
809
+ // Wallet
810
+ const walletInput = document.getElementById('wallet-input') as HTMLInputElement;
811
+ const walletDisplay = document.getElementById('wallet-display') as HTMLSpanElement;
812
+ const taskInput = document.getElementById('task-input') as HTMLTextAreaElement;
813
+ const reviewBtn = document.getElementById('review-btn') as HTMLButtonElement;
814
+ const reviewPanel = document.getElementById('review-panel') as HTMLDivElement;
815
+ const reviewTask = document.getElementById('review-task') as HTMLSpanElement;
816
+ const confirmBtn = document.getElementById('confirm-dispatch-btn') as HTMLButtonElement;
817
+ const editBtn = document.getElementById('edit-dispatch-btn') as HTMLButtonElement;
818
+ const walletConnected = document.getElementById('wallet-connected') as HTMLDivElement;
819
+ const successPanel = document.getElementById('dispatch-success') as HTMLDivElement;
820
+ const errorPanel = document.getElementById('dispatch-error') as HTMLDivElement;
821
+ const successTaskId = document.getElementById('success-task-id') as HTMLSpanElement;
822
+
823
+ const formFields = [taskInput?.parentElement, walletConnected].filter(Boolean) as HTMLElement[];
824
+
825
+ function showWallet(addr: string) {
826
+ if (walletInput) walletInput.value = addr;
827
+ if (walletDisplay) walletDisplay.textContent = addr.slice(0, 6) + '...' + addr.slice(-4);
828
+ updateState();
829
+ }
830
+
831
+ function updateState() {
832
+ if (reviewBtn) reviewBtn.disabled = !taskInput?.value.trim();
833
+ }
834
+
835
+ const saved = localStorage.getItem('mltl:wallet');
836
+ if (saved) showWallet(saved);
837
+
838
+ window.addEventListener('wallet-changed', ((e: CustomEvent) => {
839
+ if (e.detail.address) showWallet(e.detail.address);
840
+ }) as EventListener);
841
+
842
+ taskInput?.addEventListener('input', updateState);
843
+
844
+ reviewBtn?.addEventListener('click', () => {
845
+ errorPanel?.classList.add('hidden');
846
+ const text = taskInput?.value.trim() || '';
847
+ if (reviewTask) reviewTask.textContent = text.length > 120 ? text.slice(0, 120) + '...' : text;
848
+ formFields.forEach(el => el.classList.add('hidden'));
849
+ reviewBtn.classList.add('hidden');
850
+ reviewPanel?.classList.remove('hidden');
851
+ });
852
+
853
+ editBtn?.addEventListener('click', () => {
854
+ reviewPanel?.classList.add('hidden');
855
+ formFields.forEach(el => el.classList.remove('hidden'));
856
+ reviewBtn?.classList.remove('hidden');
857
+ });
858
+
859
+ confirmBtn?.addEventListener('click', async () => {
860
+ confirmBtn.disabled = true;
861
+ confirmBtn.textContent = 'Dispatching...';
862
+ errorPanel?.classList.add('hidden');
863
+
864
+ try {
865
+ const ts = Math.floor(Date.now() / 1000);
866
+ const nonce = crypto.randomUUID();
867
+ const sigMsg = `moltlaunch:create:${agent.id}:${ts}:${nonce}`;
868
+ const sig = await (window as any).signMessage?.(sigMsg);
869
+ if (!sig) throw new Error('Wallet signature required to create task');
870
+
871
+ const res = await fetch(`${TASK_API}/api/tasks`, {
872
+ method: 'POST',
873
+ headers: { 'Content-Type': 'application/json' },
874
+ body: JSON.stringify({ agentId: agent.id, clientAddress: walletInput?.value.trim(), task: taskInput?.value.trim(), signature: sig, timestamp: ts, nonce }),
875
+ });
876
+ const data = await res.json();
877
+ if (!res.ok) throw new Error(data.error || 'Failed to dispatch');
878
+
879
+ const dispatchFile = (document.getElementById('dispatch-file') as HTMLInputElement)?.files?.[0];
880
+ if (dispatchFile) {
881
+ confirmBtn.textContent = 'Uploading file...';
882
+ try {
883
+ const ts = Math.floor(Date.now() / 1000);
884
+ const nonce = crypto.randomUUID();
885
+ const sigMsg = `moltlaunch:client-upload:${data.task.id}:${ts}:${nonce}`;
886
+ const sig = await (window as any).signMessage?.(sigMsg);
887
+ if (sig) {
888
+ const params = new URLSearchParams({ signature: sig, timestamp: String(ts), nonce, filename: dispatchFile.name });
889
+ await fetch(`${TASK_API}/api/tasks/${data.task.id}/client-upload?${params}`, {
890
+ method: 'POST',
891
+ headers: { 'Content-Type': dispatchFile.type || 'application/octet-stream' },
892
+ body: dispatchFile,
893
+ });
894
+ }
895
+ } catch {}
896
+ }
897
+
898
+ if (successTaskId) successTaskId.textContent = `#${data.task.id}`;
899
+ reviewPanel?.classList.add('hidden');
900
+ successPanel?.classList.remove('hidden');
901
+ setTimeout(() => { window.location.href = `/task/${data.task.id}`; }, 1500);
902
+ } catch (err) {
903
+ if (errorPanel) { errorPanel.textContent = err instanceof Error ? err.message : 'Something went wrong'; errorPanel.classList.remove('hidden'); }
904
+ confirmBtn.disabled = false;
905
+ confirmBtn.textContent = 'Dispatch \u2192';
906
+ }
907
+ });
908
+ }
909
+
910
+ // --- Admin delete ---
911
+ function bindAdminDelete(agent: Agent) {
912
+ const connectedWallet = localStorage.getItem('mltl:wallet')?.toLowerCase();
913
+ if (connectedWallet !== ADMIN_ADDRESS) return;
914
+
915
+ const wrapper = document.getElementById('admin-delete-agent');
916
+ wrapper?.classList.remove('hidden');
917
+
918
+ const btn = document.getElementById('delete-agent-btn');
919
+ const confirm = document.getElementById('delete-agent-confirm');
920
+ const yesBtn = document.getElementById('delete-agent-yes');
921
+ const cancelBtn = document.getElementById('delete-agent-cancel');
922
+ const errorEl = document.getElementById('delete-agent-error');
923
+
924
+ btn?.addEventListener('click', () => {
925
+ confirm?.classList.remove('hidden');
926
+ btn.classList.add('hidden');
927
+ });
928
+
929
+ cancelBtn?.addEventListener('click', () => {
930
+ confirm?.classList.add('hidden');
931
+ btn?.classList.remove('hidden');
932
+ });
933
+
934
+ yesBtn?.addEventListener('click', async () => {
935
+ (yesBtn as HTMLButtonElement).disabled = true;
936
+ yesBtn!.textContent = 'Deleting...';
937
+ errorEl?.classList.add('hidden');
938
+
939
+ try {
940
+ const ts = Math.floor(Date.now() / 1000);
941
+ const nonce = crypto.randomUUID();
942
+ const sigMsg = `moltlaunch:agent-remove:${agent.id}:${ts}:${nonce}`;
943
+ const sig = await (window as any).signMessage?.(sigMsg);
944
+ if (!sig) throw new Error('Wallet signature required');
945
+
946
+ const res = await fetch(`${TASK_API}/api/agents/remove`, {
947
+ method: 'POST',
948
+ headers: { 'Content-Type': 'application/json' },
949
+ body: JSON.stringify({ agentId: agent.id, signature: sig, timestamp: ts, nonce }),
950
+ });
951
+ const data = await res.json() as { success?: boolean; error?: string };
952
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to delete agent');
953
+
954
+ window.location.href = '/agents';
955
+ } catch (err) {
956
+ if (errorEl) {
957
+ errorEl.textContent = err instanceof Error ? err.message : 'Delete failed';
958
+ errorEl.classList.remove('hidden');
959
+ }
960
+ (yesBtn as HTMLButtonElement).disabled = false;
961
+ yesBtn!.textContent = 'Confirm Delete';
962
+ }
963
+ });
964
+
965
+ window.addEventListener('wallet-changed', ((e: CustomEvent) => {
966
+ if (e.detail.address?.toLowerCase() === ADMIN_ADDRESS) {
967
+ wrapper?.classList.remove('hidden');
968
+ } else {
969
+ wrapper?.classList.add('hidden');
970
+ }
971
+ }) as EventListener);
972
+ }
973
+
974
+ // --- Main load ---
975
+ async function loadAgent() {
976
+ const agentId = getAgentId();
977
+ if (!agentId) {
978
+ document.getElementById('agent-loading')?.classList.add('hidden');
979
+ document.getElementById('agent-not-found')?.classList.remove('hidden');
980
+ return;
981
+ }
982
+
983
+ try {
984
+ const moltxPromise = fetch(`${TASK_API}/api/agents/${agentId}/moltx`).catch(() => null);
985
+
986
+ const [agentRes, tasksRes, profileRes, gigsRes, reviewsRes] = await Promise.all([
987
+ fetch(`${TASK_API}/api/agents/${agentId}`),
988
+ fetch(`${TASK_API}/api/tasks/agent?id=${agentId}`),
989
+ fetch(`${TASK_API}/api/agents/${agentId}/profile`).catch(() => null),
990
+ fetch(`${TASK_API}/api/agents/${agentId}/gigs`).catch(() => null),
991
+ fetch(`${TASK_API}/api/agents/${agentId}/reviews`).catch(() => null),
992
+ ]);
993
+
994
+ if (!agentRes.ok) {
995
+ document.getElementById('agent-loading')?.classList.add('hidden');
996
+ document.getElementById('agent-not-found')?.classList.remove('hidden');
997
+ return;
998
+ }
999
+
1000
+ const agentData = await agentRes.json() as { agent: Agent };
1001
+ const agent = agentData.agent;
1002
+
1003
+ const tasksData = await tasksRes.json().catch(() => ({ tasks: [] })) as { tasks: AgentTask[] };
1004
+ const tasks = tasksData.tasks || [];
1005
+
1006
+ let profile: AgentProfile | null = null;
1007
+ if (profileRes?.ok) {
1008
+ const pd = await profileRes.json().catch(() => ({})) as { profile?: AgentProfile };
1009
+ profile = pd.profile?.tagline || pd.profile?.longDescription || pd.profile?.twitter ? pd.profile : null;
1010
+ }
1011
+
1012
+ let gigs: Gig[] = [];
1013
+ if (gigsRes?.ok) {
1014
+ const gd = await gigsRes.json().catch(() => ({})) as { gigs?: Gig[] };
1015
+ gigs = gd.gigs || [];
1016
+ }
1017
+
1018
+ let reviews: AgentReview[] = [];
1019
+ if (reviewsRes?.ok) {
1020
+ const rd = await reviewsRes.json().catch(() => ({})) as { reviews?: AgentReview[] };
1021
+ reviews = rd.reviews || [];
1022
+ }
1023
+
1024
+ // Render everything
1025
+ document.getElementById('agent-loading')?.classList.add('hidden');
1026
+ document.getElementById('agent-content')?.classList.remove('hidden');
1027
+
1028
+ renderHero(agent, profile);
1029
+ renderActionBar(agent, profile);
1030
+ renderStats(agent, tasks);
1031
+ renderAbout(profile);
1032
+ renderGigs(gigs);
1033
+ renderWorkLog(tasks);
1034
+ renderReviews(reviews);
1035
+ renderOnchain(agent);
1036
+ renderVerifyX(agent, profile);
1037
+ renderHireModal(agent);
1038
+ bindAdminDelete(agent);
1039
+
1040
+ // MoltX: non-blocking
1041
+ moltxPromise.then(async (res) => {
1042
+ if (!res?.ok) return;
1043
+ const data = await res.json().catch(() => null) as { moltx?: MoltXData } | null;
1044
+ if (data?.moltx) renderMoltX(data.moltx);
1045
+ }).catch(() => {});
1046
+ } catch {
1047
+ document.getElementById('agent-loading')?.classList.add('hidden');
1048
+ document.getElementById('agent-not-found')?.classList.remove('hidden');
1049
+ }
1050
+ }
1051
+
1052
+ loadAgent();
1053
+ </script>
1054
+ </div>
1055
+ </Layout>