@nordsym/apiclaw 1.0.0 → 1.1.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 (161) hide show
  1. package/AGENTS.md +74 -0
  2. package/HEARTBEAT.md +4 -0
  3. package/IDENTITY.md +22 -0
  4. package/README.md +193 -202
  5. package/SOUL.md +36 -0
  6. package/STATUS.md +237 -0
  7. package/TOOLS.md +36 -0
  8. package/USER.md +17 -0
  9. package/{backend/convex → convex}/_generated/api.d.ts +12 -6
  10. package/convex/analytics.ts +90 -0
  11. package/convex/credits.ts +211 -0
  12. package/convex/http.ts +578 -0
  13. package/convex/providers.ts +516 -0
  14. package/convex/purchases.ts +183 -0
  15. package/convex/ratelimit.ts +104 -0
  16. package/convex/schema.ts +220 -0
  17. package/convex/telemetry.ts +81 -0
  18. package/convex.json +3 -0
  19. package/dist/credentials.d.ts +19 -0
  20. package/dist/credentials.d.ts.map +1 -0
  21. package/dist/credentials.js +158 -0
  22. package/dist/credentials.js.map +1 -0
  23. package/dist/credits.d.ts +14 -11
  24. package/dist/credits.d.ts.map +1 -1
  25. package/dist/credits.js +151 -99
  26. package/dist/credits.js.map +1 -1
  27. package/dist/discovery.d.ts +7 -16
  28. package/dist/discovery.d.ts.map +1 -1
  29. package/dist/discovery.js +33 -40
  30. package/dist/discovery.js.map +1 -1
  31. package/dist/execute.d.ts +19 -0
  32. package/dist/execute.d.ts.map +1 -0
  33. package/dist/execute.js +285 -0
  34. package/dist/execute.js.map +1 -0
  35. package/dist/index.js +175 -31
  36. package/dist/index.js.map +1 -1
  37. package/dist/proxy.d.ts +6 -0
  38. package/dist/proxy.d.ts.map +1 -0
  39. package/dist/proxy.js +19 -0
  40. package/dist/proxy.js.map +1 -0
  41. package/dist/registry/apis.json +95362 -202
  42. package/dist/registry/apis_expanded.json +100853 -0
  43. package/dist/stripe.d.ts +68 -0
  44. package/dist/stripe.d.ts.map +1 -0
  45. package/dist/stripe.js +196 -0
  46. package/dist/stripe.js.map +1 -0
  47. package/dist/telemetry.d.ts +28 -0
  48. package/dist/telemetry.d.ts.map +1 -0
  49. package/dist/telemetry.js +50 -0
  50. package/dist/telemetry.js.map +1 -0
  51. package/dist/test.d.ts +3 -2
  52. package/dist/test.d.ts.map +1 -1
  53. package/dist/test.js +105 -75
  54. package/dist/test.js.map +1 -1
  55. package/dist/types.d.ts +0 -28
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/webhook.d.ts +2 -0
  58. package/dist/webhook.d.ts.map +1 -0
  59. package/dist/webhook.js +90 -0
  60. package/dist/webhook.js.map +1 -0
  61. package/landing/DESIGN.md +343 -0
  62. package/landing/package-lock.json +1196 -7
  63. package/landing/package.json +5 -1
  64. package/landing/public/android-chrome-192x192.png +0 -0
  65. package/landing/public/android-chrome-512x512.png +0 -0
  66. package/landing/public/apple-touch-icon.png +0 -0
  67. package/landing/public/demo.gif +0 -0
  68. package/landing/public/demo.mp4 +0 -0
  69. package/landing/public/favicon-16x16.png +0 -0
  70. package/landing/public/favicon-32x32.png +0 -0
  71. package/landing/public/favicon.ico +0 -0
  72. package/landing/public/favicon.svg +3 -0
  73. package/landing/public/icon.svg +47 -0
  74. package/landing/public/logo-mono.svg +37 -0
  75. package/landing/public/logo-simple.svg +45 -0
  76. package/landing/public/logo.svg +84 -0
  77. package/landing/public/og-template.html +184 -0
  78. package/landing/public/site.webmanifest +31 -0
  79. package/landing/scripts/generate-assets.js +284 -0
  80. package/landing/scripts/generate-pngs.js +48 -0
  81. package/landing/scripts/generate-stats.js +42 -0
  82. package/landing/src/app/admin/page.tsx +348 -0
  83. package/landing/src/app/api/auth/magic-link/route.ts +73 -0
  84. package/landing/src/app/api/auth/session/route.ts +38 -0
  85. package/landing/src/app/api/auth/verify/route.ts +43 -0
  86. package/landing/src/app/api/og/route.tsx +84 -0
  87. package/landing/src/app/globals.css +439 -100
  88. package/landing/src/app/layout.tsx +37 -7
  89. package/landing/src/app/page.tsx +627 -552
  90. package/landing/src/app/providers/dashboard/login/page.tsx +176 -0
  91. package/landing/src/app/providers/dashboard/page.tsx +589 -0
  92. package/landing/src/app/providers/dashboard/verify/page.tsx +106 -0
  93. package/landing/src/app/providers/layout.tsx +14 -0
  94. package/landing/src/app/providers/page.tsx +402 -0
  95. package/landing/src/app/providers/register/page.tsx +670 -0
  96. package/landing/src/components/ProviderDashboard.tsx +794 -0
  97. package/landing/src/hooks/useDashboardData.ts +99 -0
  98. package/landing/src/lib/apis.json +116054 -0
  99. package/landing/src/lib/convex-client.ts +106 -0
  100. package/landing/src/lib/mock-data.ts +285 -0
  101. package/landing/src/lib/stats.json +6 -0
  102. package/landing/tailwind.config.ts +12 -11
  103. package/landing/tsconfig.tsbuildinfo +1 -0
  104. package/package.json +21 -20
  105. package/scripts/SYMBOT-FIX.md +238 -0
  106. package/scripts/demo-simulation.py +177 -0
  107. package/scripts/expand-more.py +502 -0
  108. package/scripts/expand-registry.py +434 -0
  109. package/scripts/history-sanitizer.ts +272 -0
  110. package/scripts/mass-scrape.py +1308 -0
  111. package/scripts/sync-and-deploy.sh +36 -0
  112. package/src/credentials.ts +177 -0
  113. package/src/credits.ts +190 -122
  114. package/src/discovery.ts +45 -58
  115. package/src/execute.ts +350 -0
  116. package/src/index.ts +184 -32
  117. package/src/proxy.ts +24 -0
  118. package/src/registry/apis.json +95362 -202
  119. package/src/registry/apis_expanded.json +100853 -0
  120. package/src/stripe.ts +243 -0
  121. package/src/telemetry.ts +71 -0
  122. package/src/test.ts +127 -89
  123. package/src/types.ts +0 -34
  124. package/src/webhook.ts +107 -0
  125. package/.github/ISSUE_TEMPLATE/add-api.yml +0 -123
  126. package/BRIEFING.md +0 -30
  127. package/backend/convex/apiKeys.ts +0 -75
  128. package/backend/convex/purchases.ts +0 -74
  129. package/backend/convex/schema.ts +0 -45
  130. package/backend/convex/transactions.ts +0 -57
  131. package/backend/convex/users.ts +0 -94
  132. package/backend/package-lock.json +0 -521
  133. package/backend/package.json +0 -15
  134. package/dist/registry/parse_apis.py +0 -146
  135. package/dist/revenuecat.d.ts +0 -61
  136. package/dist/revenuecat.d.ts.map +0 -1
  137. package/dist/revenuecat.js +0 -166
  138. package/dist/revenuecat.js.map +0 -1
  139. package/dist/webhooks/revenuecat.d.ts +0 -48
  140. package/dist/webhooks/revenuecat.d.ts.map +0 -1
  141. package/dist/webhooks/revenuecat.js +0 -119
  142. package/dist/webhooks/revenuecat.js.map +0 -1
  143. package/docs/revenuecat-setup.md +0 -89
  144. package/landing/src/app/api/keys/route.ts +0 -71
  145. package/landing/src/app/api/log/route.ts +0 -37
  146. package/landing/src/app/api/stats/route.ts +0 -37
  147. package/landing/src/app/page.tsx.bak +0 -567
  148. package/landing/src/components/AddKeyModal.tsx +0 -159
  149. package/newsletter-template.html +0 -71
  150. package/outreach/OUTREACH-SYSTEM.md +0 -211
  151. package/outreach/email-template.html +0 -179
  152. package/outreach/targets.md +0 -133
  153. package/src/registry/parse_apis.py +0 -146
  154. package/src/revenuecat.ts +0 -239
  155. package/src/webhooks/revenuecat.ts +0 -187
  156. /package/{backend/convex → convex}/README.md +0 -0
  157. /package/{backend/convex → convex}/_generated/api.js +0 -0
  158. /package/{backend/convex → convex}/_generated/dataModel.d.ts +0 -0
  159. /package/{backend/convex → convex}/_generated/server.d.ts +0 -0
  160. /package/{backend/convex → convex}/_generated/server.js +0 -0
  161. /package/{backend/convex → convex}/tsconfig.json +0 -0
@@ -0,0 +1,794 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ BarChart3,
6
+ CreditCard,
7
+ Settings,
8
+ TrendingUp,
9
+ Users,
10
+ Zap,
11
+ DollarSign,
12
+ ArrowUpRight,
13
+ ArrowDownRight,
14
+ Eye,
15
+ EyeOff,
16
+ Copy,
17
+ RefreshCw,
18
+ Pause,
19
+ Play,
20
+ ExternalLink,
21
+ ChevronRight,
22
+ Check,
23
+ Clock,
24
+ AlertCircle,
25
+ } from "lucide-react";
26
+ import {
27
+ LineChart,
28
+ Line,
29
+ BarChart,
30
+ Bar,
31
+ XAxis,
32
+ YAxis,
33
+ CartesianGrid,
34
+ Tooltip,
35
+ ResponsiveContainer,
36
+ PieChart,
37
+ Pie,
38
+ Cell,
39
+ } from "recharts";
40
+ import {
41
+ getMockProvider,
42
+ getMockApis,
43
+ getMockAnalytics,
44
+ getMockEarnings,
45
+ getMockCredentials,
46
+ type Api,
47
+ } from "@/lib/mock-data";
48
+
49
+ type TabType = "overview" | "apis" | "earnings";
50
+
51
+ const COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6"];
52
+
53
+ export function ProviderDashboard() {
54
+ const [activeTab, setActiveTab] = useState<TabType>("overview");
55
+ const [period, setPeriod] = useState<"week" | "month" | "all">("month");
56
+ const [selectedApi, setSelectedApi] = useState<string | null>(null);
57
+
58
+ const provider = getMockProvider();
59
+ const apis = getMockApis();
60
+ const analytics = getMockAnalytics();
61
+ const earnings = getMockEarnings();
62
+
63
+ const tabs = [
64
+ { id: "overview" as TabType, label: "Overview", icon: BarChart3 },
65
+ { id: "apis" as TabType, label: "APIs", icon: Zap },
66
+ { id: "earnings" as TabType, label: "Earnings", icon: CreditCard },
67
+ ];
68
+
69
+ return (
70
+ <div className="min-h-screen bg-background">
71
+ {/* Header */}
72
+ <header className="border-b border-border bg-surface/50 backdrop-blur-xl sticky top-0 z-50">
73
+ <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
74
+ <div className="flex items-center gap-4">
75
+ <div className="w-10 h-10 rounded-xl bg-accent/20 flex items-center justify-center text-xl">
76
+ 🦞
77
+ </div>
78
+ <div>
79
+ <h1 className="font-bold text-lg">Provider Dashboard</h1>
80
+ <p className="text-sm text-text-muted">{provider.company || provider.email}</p>
81
+ </div>
82
+ </div>
83
+ <div className="flex items-center gap-3">
84
+ <button className="p-2 rounded-lg hover:bg-surface transition">
85
+ <Settings className="w-5 h-5 text-text-muted" />
86
+ </button>
87
+ <div className="w-9 h-9 rounded-full bg-accent/20 flex items-center justify-center text-sm font-medium">
88
+ {provider.name.charAt(0).toUpperCase()}
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </header>
93
+
94
+ <div className="max-w-7xl mx-auto px-6 py-8">
95
+ {/* Tab Navigation */}
96
+ <div className="flex items-center gap-1 p-1 bg-surface rounded-xl w-fit mb-8">
97
+ {tabs.map((tab) => (
98
+ <button
99
+ key={tab.id}
100
+ onClick={() => setActiveTab(tab.id)}
101
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
102
+ activeTab === tab.id
103
+ ? "bg-accent text-white"
104
+ : "text-text-muted hover:text-text-primary"
105
+ }`}
106
+ >
107
+ <tab.icon className="w-4 h-4" />
108
+ {tab.label}
109
+ </button>
110
+ ))}
111
+ </div>
112
+
113
+ {/* Tab Content */}
114
+ {activeTab === "overview" && (
115
+ <OverviewTab
116
+ analytics={analytics}
117
+ period={period}
118
+ setPeriod={setPeriod}
119
+ />
120
+ )}
121
+ {activeTab === "apis" && (
122
+ <ApisTab
123
+ apis={apis}
124
+ selectedApi={selectedApi}
125
+ setSelectedApi={setSelectedApi}
126
+ />
127
+ )}
128
+ {activeTab === "earnings" && <EarningsTab earnings={earnings} />}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ // ============================================
135
+ // OVERVIEW TAB
136
+ // ============================================
137
+
138
+ function OverviewTab({
139
+ analytics,
140
+ period,
141
+ setPeriod,
142
+ }: {
143
+ analytics: ReturnType<typeof getMockAnalytics>;
144
+ period: "week" | "month" | "all";
145
+ setPeriod: (p: "week" | "month" | "all") => void;
146
+ }) {
147
+ const periodData = {
148
+ week: {
149
+ calls: analytics.totalCallsWeek,
150
+ revenue: analytics.revenueWeek,
151
+ chartData: analytics.callsByDay.slice(-7),
152
+ },
153
+ month: {
154
+ calls: analytics.totalCallsMonth,
155
+ revenue: analytics.revenueMonth,
156
+ chartData: analytics.callsByDay.slice(-30),
157
+ },
158
+ all: {
159
+ calls: analytics.totalCalls,
160
+ revenue: analytics.totalRevenue,
161
+ chartData: analytics.callsByDay,
162
+ },
163
+ }[period];
164
+
165
+ // Calculate growth
166
+ const prevPeriodCalls =
167
+ period === "week"
168
+ ? analytics.callsByDay.slice(-14, -7).reduce((s, d) => s + d.calls, 0)
169
+ : period === "month"
170
+ ? analytics.callsByDay.slice(-60, -30).reduce((s, d) => s + d.calls, 0)
171
+ : 0;
172
+ const callsGrowth =
173
+ prevPeriodCalls > 0
174
+ ? ((periodData.calls - prevPeriodCalls) / prevPeriodCalls) * 100
175
+ : 0;
176
+
177
+ return (
178
+ <div className="space-y-8">
179
+ {/* Period Selector */}
180
+ <div className="flex items-center justify-between">
181
+ <h2 className="text-2xl font-bold">Analytics</h2>
182
+ <div className="flex items-center gap-1 p-1 bg-surface rounded-lg">
183
+ {(["week", "month", "all"] as const).map((p) => (
184
+ <button
185
+ key={p}
186
+ onClick={() => setPeriod(p)}
187
+ className={`px-3 py-1.5 rounded-md text-sm font-medium transition ${
188
+ period === p
189
+ ? "bg-surface-elevated text-text-primary"
190
+ : "text-text-muted hover:text-text-primary"
191
+ }`}
192
+ >
193
+ {p === "week" ? "7 days" : p === "month" ? "30 days" : "All time"}
194
+ </button>
195
+ ))}
196
+ </div>
197
+ </div>
198
+
199
+ {/* Stats Grid */}
200
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
201
+ <StatCard
202
+ title="Total Calls"
203
+ value={periodData.calls.toLocaleString()}
204
+ change={callsGrowth}
205
+ icon={Zap}
206
+ />
207
+ <StatCard
208
+ title="Unique Agents"
209
+ value={analytics.uniqueAgents.toString()}
210
+ icon={Users}
211
+ />
212
+ <StatCard
213
+ title="Revenue"
214
+ value={`$${periodData.revenue.toFixed(2)}`}
215
+ icon={DollarSign}
216
+ accent
217
+ />
218
+ <StatCard
219
+ title="Avg Calls/Day"
220
+ value={Math.round(periodData.calls / periodData.chartData.length).toString()}
221
+ icon={TrendingUp}
222
+ />
223
+ </div>
224
+
225
+ {/* Charts */}
226
+ <div className="grid lg:grid-cols-3 gap-6">
227
+ {/* Line Chart - Calls Over Time */}
228
+ <div className="lg:col-span-2 bg-surface-elevated rounded-2xl border border-border p-6">
229
+ <h3 className="font-semibold mb-4">Calls Over Time</h3>
230
+ <div className="h-80">
231
+ <ResponsiveContainer width="100%" height="100%">
232
+ <LineChart data={periodData.chartData}>
233
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
234
+ <XAxis
235
+ dataKey="date"
236
+ tick={{ fontSize: 12, fill: "var(--text-muted)" }}
237
+ tickFormatter={(d) => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
238
+ />
239
+ <YAxis tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
240
+ <Tooltip
241
+ contentStyle={{
242
+ background: "var(--surface-elevated)",
243
+ border: "1px solid var(--border)",
244
+ borderRadius: "8px",
245
+ }}
246
+ labelFormatter={(d) => new Date(d).toLocaleDateString()}
247
+ />
248
+ <Line
249
+ type="monotone"
250
+ dataKey="calls"
251
+ stroke="#ef4444"
252
+ strokeWidth={2}
253
+ dot={false}
254
+ activeDot={{ r: 4, fill: "#ef4444" }}
255
+ />
256
+ </LineChart>
257
+ </ResponsiveContainer>
258
+ </div>
259
+ </div>
260
+
261
+ {/* Top Agents */}
262
+ <div className="bg-surface-elevated rounded-2xl border border-border p-6">
263
+ <h3 className="font-semibold mb-4">Top Agents</h3>
264
+ <div className="space-y-3">
265
+ {analytics.topAgents.slice(0, 6).map((agent, i) => (
266
+ <div key={agent.agentId} className="flex items-center justify-between">
267
+ <div className="flex items-center gap-3">
268
+ <span className="w-6 h-6 rounded-full bg-surface flex items-center justify-center text-xs font-medium text-text-muted">
269
+ {i + 1}
270
+ </span>
271
+ <span className="text-sm font-mono truncate max-w-[140px]">
272
+ {agent.agentId.replace("agent_", "")}
273
+ </span>
274
+ </div>
275
+ <span className="text-sm text-text-muted">{agent.calls.toLocaleString()}</span>
276
+ </div>
277
+ ))}
278
+ </div>
279
+ </div>
280
+ </div>
281
+
282
+ {/* Second Row - Bar Chart & Pie Chart */}
283
+ <div className="grid lg:grid-cols-2 gap-6">
284
+ {/* Calls per API */}
285
+ <div className="bg-surface-elevated rounded-2xl border border-border p-6">
286
+ <h3 className="font-semibold mb-4">Calls per API</h3>
287
+ <div className="h-64">
288
+ <ResponsiveContainer width="100%" height="100%">
289
+ <BarChart data={analytics.apis} layout="vertical">
290
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
291
+ <XAxis type="number" tick={{ fontSize: 12, fill: "var(--text-muted)" }} />
292
+ <YAxis
293
+ type="category"
294
+ dataKey="name"
295
+ tick={{ fontSize: 12, fill: "var(--text-muted)" }}
296
+ width={120}
297
+ />
298
+ <Tooltip
299
+ contentStyle={{
300
+ background: "var(--surface-elevated)",
301
+ border: "1px solid var(--border)",
302
+ borderRadius: "8px",
303
+ }}
304
+ />
305
+ <Bar dataKey="calls" fill="#ef4444" radius={[0, 4, 4, 0]} />
306
+ </BarChart>
307
+ </ResponsiveContainer>
308
+ </div>
309
+ </div>
310
+
311
+ {/* Geographic Distribution */}
312
+ <div className="bg-surface-elevated rounded-2xl border border-border p-6">
313
+ <h3 className="font-semibold mb-4">Geographic Distribution</h3>
314
+ <div className="h-64 flex items-center justify-center">
315
+ <ResponsiveContainer width="100%" height="100%">
316
+ <PieChart>
317
+ <Pie
318
+ data={Object.entries(analytics.callsByRegion).map(([name, value]) => ({
319
+ name,
320
+ value,
321
+ }))}
322
+ cx="50%"
323
+ cy="50%"
324
+ innerRadius={60}
325
+ outerRadius={90}
326
+ paddingAngle={2}
327
+ dataKey="value"
328
+ >
329
+ {Object.keys(analytics.callsByRegion).map((_, index) => (
330
+ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
331
+ ))}
332
+ </Pie>
333
+ <Tooltip
334
+ contentStyle={{
335
+ background: "var(--surface-elevated)",
336
+ border: "1px solid var(--border)",
337
+ borderRadius: "8px",
338
+ }}
339
+ />
340
+ </PieChart>
341
+ </ResponsiveContainer>
342
+ </div>
343
+ <div className="flex flex-wrap justify-center gap-4 mt-4">
344
+ {Object.entries(analytics.callsByRegion).map(([region, calls], i) => (
345
+ <div key={region} className="flex items-center gap-2">
346
+ <div
347
+ className="w-3 h-3 rounded-full"
348
+ style={{ background: COLORS[i % COLORS.length] }}
349
+ />
350
+ <span className="text-sm text-text-muted">
351
+ {region}: {calls.toLocaleString()}
352
+ </span>
353
+ </div>
354
+ ))}
355
+ </div>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ );
360
+ }
361
+
362
+ function StatCard({
363
+ title,
364
+ value,
365
+ change,
366
+ icon: Icon,
367
+ accent,
368
+ }: {
369
+ title: string;
370
+ value: string;
371
+ change?: number;
372
+ icon: typeof Zap;
373
+ accent?: boolean;
374
+ }) {
375
+ return (
376
+ <div className={`rounded-2xl border p-5 ${accent ? "bg-accent/10 border-accent/30" : "bg-surface-elevated border-border"}`}>
377
+ <div className="flex items-center justify-between mb-3">
378
+ <span className="text-sm text-text-muted">{title}</span>
379
+ <Icon className={`w-5 h-5 ${accent ? "text-accent" : "text-text-muted"}`} />
380
+ </div>
381
+ <div className="flex items-end justify-between">
382
+ <span className={`text-3xl font-bold ${accent ? "text-accent" : ""}`}>{value}</span>
383
+ {change !== undefined && (
384
+ <div className={`flex items-center gap-1 text-sm ${change >= 0 ? "text-green-500" : "text-red-500"}`}>
385
+ {change >= 0 ? <ArrowUpRight className="w-4 h-4" /> : <ArrowDownRight className="w-4 h-4" />}
386
+ {Math.abs(change).toFixed(1)}%
387
+ </div>
388
+ )}
389
+ </div>
390
+ </div>
391
+ );
392
+ }
393
+
394
+ // ============================================
395
+ // APIS TAB
396
+ // ============================================
397
+
398
+ function ApisTab({
399
+ apis,
400
+ selectedApi,
401
+ setSelectedApi,
402
+ }: {
403
+ apis: Api[];
404
+ selectedApi: string | null;
405
+ setSelectedApi: (id: string | null) => void;
406
+ }) {
407
+ const selected = apis.find((a) => a.id === selectedApi);
408
+
409
+ return (
410
+ <div className="space-y-6">
411
+ <div className="flex items-center justify-between">
412
+ <h2 className="text-2xl font-bold">Your APIs</h2>
413
+ <button className="btn-primary !py-2 !px-4 text-sm">
414
+ <Zap className="w-4 h-4" />
415
+ Add API
416
+ </button>
417
+ </div>
418
+
419
+ <div className="grid lg:grid-cols-3 gap-6">
420
+ {/* API List */}
421
+ <div className="lg:col-span-1 space-y-3">
422
+ {apis.map((api) => (
423
+ <button
424
+ key={api.id}
425
+ onClick={() => setSelectedApi(api.id)}
426
+ className={`w-full text-left rounded-xl border p-4 transition-all ${
427
+ selectedApi === api.id
428
+ ? "bg-accent/10 border-accent"
429
+ : "bg-surface-elevated border-border hover:border-accent/50"
430
+ }`}
431
+ >
432
+ <div className="flex items-center justify-between mb-2">
433
+ <div className="flex items-center gap-2">
434
+ <span className="text-2xl">{api.icon}</span>
435
+ <span className="font-semibold">{api.name}</span>
436
+ </div>
437
+ <span
438
+ className={`px-2 py-0.5 rounded-full text-xs font-medium ${
439
+ api.status === "active"
440
+ ? "bg-green-500/20 text-green-500"
441
+ : "bg-yellow-500/20 text-yellow-600"
442
+ }`}
443
+ >
444
+ {api.status}
445
+ </span>
446
+ </div>
447
+ <p className="text-sm text-text-muted line-clamp-2">{api.description}</p>
448
+ <div className="flex items-center gap-4 mt-3 text-sm text-text-muted">
449
+ <span>{api.calls.toLocaleString()} calls</span>
450
+ <span>{api.category}</span>
451
+ </div>
452
+ </button>
453
+ ))}
454
+ </div>
455
+
456
+ {/* API Details */}
457
+ <div className="lg:col-span-2">
458
+ {selected ? (
459
+ <ApiDetails api={selected} />
460
+ ) : (
461
+ <div className="h-full flex items-center justify-center rounded-2xl border border-dashed border-border bg-surface/50 p-12">
462
+ <p className="text-text-muted">Select an API to view details</p>
463
+ </div>
464
+ )}
465
+ </div>
466
+ </div>
467
+ </div>
468
+ );
469
+ }
470
+
471
+ function ApiDetails({ api }: { api: Api }) {
472
+ const [showSecret, setShowSecret] = useState(false);
473
+ const [copied, setCopied] = useState<string | null>(null);
474
+ const credentials = getMockCredentials(api.id);
475
+
476
+ const copyToClipboard = (text: string, key: string) => {
477
+ navigator.clipboard.writeText(text);
478
+ setCopied(key);
479
+ setTimeout(() => setCopied(null), 2000);
480
+ };
481
+
482
+ return (
483
+ <div className="bg-surface-elevated rounded-2xl border border-border">
484
+ {/* Header */}
485
+ <div className="p-6 border-b border-border">
486
+ <div className="flex items-center justify-between mb-4">
487
+ <div className="flex items-center gap-3">
488
+ <span className="text-4xl">{api.icon}</span>
489
+ <div>
490
+ <h3 className="text-xl font-bold">{api.name}</h3>
491
+ <p className="text-sm text-text-muted">{api.category}</p>
492
+ </div>
493
+ </div>
494
+ <div className="flex items-center gap-2">
495
+ <button className="btn-secondary !py-2 !px-3 text-sm">
496
+ {api.status === "active" ? (
497
+ <>
498
+ <Pause className="w-4 h-4" /> Pause
499
+ </>
500
+ ) : (
501
+ <>
502
+ <Play className="w-4 h-4" /> Resume
503
+ </>
504
+ )}
505
+ </button>
506
+ <button className="btn-secondary !py-2 !px-3 text-sm">
507
+ <Settings className="w-4 h-4" /> Edit
508
+ </button>
509
+ </div>
510
+ </div>
511
+ <p className="text-text-secondary">{api.description}</p>
512
+ </div>
513
+
514
+ {/* Details Grid */}
515
+ <div className="grid md:grid-cols-2 gap-6 p-6 border-b border-border">
516
+ <div className="space-y-4">
517
+ <h4 className="font-semibold text-sm text-text-muted uppercase tracking-wider">Configuration</h4>
518
+ <div className="space-y-3">
519
+ <DetailRow label="Base URL" value={api.baseUrl} mono />
520
+ <DetailRow
521
+ label="Documentation"
522
+ value={
523
+ api.docsUrl ? (
524
+ <a href={api.docsUrl} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline flex items-center gap-1">
525
+ View Docs <ExternalLink className="w-3 h-3" />
526
+ </a>
527
+ ) : (
528
+ "Not set"
529
+ )
530
+ }
531
+ />
532
+ <DetailRow label="Auth Type" value={api.authType} />
533
+ <DetailRow label="Rate Limit" value={`${api.rateLimitPerMinute || "∞"}/min`} />
534
+ </div>
535
+ </div>
536
+ <div className="space-y-4">
537
+ <h4 className="font-semibold text-sm text-text-muted uppercase tracking-wider">Pricing</h4>
538
+ <div className="space-y-3">
539
+ <DetailRow label="Model" value={api.pricingModel} />
540
+ {api.pricePerCall && (
541
+ <DetailRow
542
+ label="Price per call"
543
+ value={`$${(api.pricePerCall / 100).toFixed(4)}`}
544
+ />
545
+ )}
546
+ <DetailRow
547
+ label="Regions"
548
+ value={api.regions?.join(", ") || "Global"}
549
+ />
550
+ <DetailRow
551
+ label="Tags"
552
+ value={
553
+ <div className="flex flex-wrap gap-1">
554
+ {api.tags?.map((tag) => (
555
+ <span key={tag} className="px-2 py-0.5 bg-surface rounded text-xs">
556
+ {tag}
557
+ </span>
558
+ ))}
559
+ </div>
560
+ }
561
+ />
562
+ </div>
563
+ </div>
564
+ </div>
565
+
566
+ {/* Credentials */}
567
+ <div className="p-6">
568
+ <div className="flex items-center justify-between mb-4">
569
+ <h4 className="font-semibold text-sm text-text-muted uppercase tracking-wider">Credentials</h4>
570
+ <button className="text-sm text-accent hover:underline flex items-center gap-1">
571
+ <RefreshCw className="w-4 h-4" /> Regenerate
572
+ </button>
573
+ </div>
574
+ <div className="space-y-3">
575
+ <div className="flex items-center justify-between p-3 bg-surface rounded-lg">
576
+ <div>
577
+ <p className="text-sm text-text-muted mb-1">API Key</p>
578
+ <p className="font-mono text-sm">{credentials.apiKey}</p>
579
+ </div>
580
+ <button
581
+ onClick={() => copyToClipboard(credentials.apiKey, "api")}
582
+ className="p-2 hover:bg-surface-elevated rounded-lg transition"
583
+ >
584
+ {copied === "api" ? (
585
+ <Check className="w-4 h-4 text-green-500" />
586
+ ) : (
587
+ <Copy className="w-4 h-4 text-text-muted" />
588
+ )}
589
+ </button>
590
+ </div>
591
+ {credentials.secretKey && (
592
+ <div className="flex items-center justify-between p-3 bg-surface rounded-lg">
593
+ <div>
594
+ <p className="text-sm text-text-muted mb-1">Secret Key</p>
595
+ <p className="font-mono text-sm">
596
+ {showSecret ? credentials.secretKey : "sk_live_••••••••••••••••••••••••"}
597
+ </p>
598
+ </div>
599
+ <div className="flex items-center gap-1">
600
+ <button
601
+ onClick={() => setShowSecret(!showSecret)}
602
+ className="p-2 hover:bg-surface-elevated rounded-lg transition"
603
+ >
604
+ {showSecret ? (
605
+ <EyeOff className="w-4 h-4 text-text-muted" />
606
+ ) : (
607
+ <Eye className="w-4 h-4 text-text-muted" />
608
+ )}
609
+ </button>
610
+ <button
611
+ onClick={() => copyToClipboard(credentials.secretKey!, "secret")}
612
+ className="p-2 hover:bg-surface-elevated rounded-lg transition"
613
+ >
614
+ {copied === "secret" ? (
615
+ <Check className="w-4 h-4 text-green-500" />
616
+ ) : (
617
+ <Copy className="w-4 h-4 text-text-muted" />
618
+ )}
619
+ </button>
620
+ </div>
621
+ </div>
622
+ )}
623
+ </div>
624
+ </div>
625
+ </div>
626
+ );
627
+ }
628
+
629
+ function DetailRow({
630
+ label,
631
+ value,
632
+ mono,
633
+ }: {
634
+ label: string;
635
+ value: React.ReactNode;
636
+ mono?: boolean;
637
+ }) {
638
+ return (
639
+ <div className="flex items-center justify-between">
640
+ <span className="text-sm text-text-muted">{label}</span>
641
+ <span className={`text-sm ${mono ? "font-mono" : ""}`}>{value}</span>
642
+ </div>
643
+ );
644
+ }
645
+
646
+ // ============================================
647
+ // EARNINGS TAB
648
+ // ============================================
649
+
650
+ function EarningsTab({ earnings }: { earnings: ReturnType<typeof getMockEarnings> }) {
651
+ return (
652
+ <div className="space-y-8">
653
+ <h2 className="text-2xl font-bold">Earnings</h2>
654
+
655
+ {/* Earnings Stats */}
656
+ <div className="grid md:grid-cols-3 gap-4">
657
+ <div className="bg-accent/10 border border-accent/30 rounded-2xl p-6">
658
+ <div className="flex items-center gap-2 mb-3">
659
+ <Clock className="w-5 h-5 text-accent" />
660
+ <span className="text-sm text-text-muted">Pending Payout</span>
661
+ </div>
662
+ <p className="text-4xl font-bold text-accent">${earnings.pendingAmount.toFixed(2)}</p>
663
+ <p className="text-sm text-text-muted mt-2">Available for payout</p>
664
+ </div>
665
+ <div className="bg-surface-elevated border border-border rounded-2xl p-6">
666
+ <div className="flex items-center gap-2 mb-3">
667
+ <DollarSign className="w-5 h-5 text-text-muted" />
668
+ <span className="text-sm text-text-muted">Total Earned</span>
669
+ </div>
670
+ <p className="text-4xl font-bold">${earnings.totalEarned.toFixed(2)}</p>
671
+ <p className="text-sm text-text-muted mt-2">All time</p>
672
+ </div>
673
+ <div className="bg-surface-elevated border border-border rounded-2xl p-6">
674
+ <div className="flex items-center gap-2 mb-3">
675
+ <Check className="w-5 h-5 text-green-500" />
676
+ <span className="text-sm text-text-muted">Total Paid Out</span>
677
+ </div>
678
+ <p className="text-4xl font-bold">${earnings.totalPaidOut.toFixed(2)}</p>
679
+ <p className="text-sm text-text-muted mt-2">Successfully transferred</p>
680
+ </div>
681
+ </div>
682
+
683
+ {/* Stripe Connect */}
684
+ <div className="bg-surface-elevated border border-border rounded-2xl p-6">
685
+ <div className="flex items-center justify-between">
686
+ <div>
687
+ <h3 className="font-semibold text-lg mb-1">Payout Settings</h3>
688
+ <p className="text-sm text-text-muted">
689
+ Connect your Stripe account to receive payouts
690
+ </p>
691
+ </div>
692
+ {earnings.stripeOnboardingComplete ? (
693
+ <div className="flex items-center gap-2 text-green-500">
694
+ <Check className="w-5 h-5" />
695
+ <span className="font-medium">Connected</span>
696
+ </div>
697
+ ) : (
698
+ <button className="btn-primary !py-2 !px-4">
699
+ <CreditCard className="w-4 h-4" />
700
+ Connect Stripe
701
+ </button>
702
+ )}
703
+ </div>
704
+ {earnings.stripeOnboardingComplete && (
705
+ <div className="mt-4 pt-4 border-t border-border flex items-center justify-between">
706
+ <div className="flex items-center gap-3">
707
+ <div className="w-10 h-10 rounded-lg bg-[#635BFF]/20 flex items-center justify-center">
708
+ <svg viewBox="0 0 24 24" className="w-6 h-6 text-[#635BFF]" fill="currentColor">
709
+ <path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z" />
710
+ </svg>
711
+ </div>
712
+ <div>
713
+ <p className="font-medium">Stripe Connect</p>
714
+ <p className="text-sm text-text-muted">Account verified and ready for payouts</p>
715
+ </div>
716
+ </div>
717
+ <button className="text-sm text-accent hover:underline flex items-center gap-1">
718
+ Manage <ChevronRight className="w-4 h-4" />
719
+ </button>
720
+ </div>
721
+ )}
722
+ </div>
723
+
724
+ {/* Payout History */}
725
+ <div className="bg-surface-elevated border border-border rounded-2xl overflow-hidden">
726
+ <div className="p-6 border-b border-border">
727
+ <h3 className="font-semibold text-lg">Payout History</h3>
728
+ </div>
729
+ {earnings.payouts.length > 0 ? (
730
+ <div className="divide-y divide-border">
731
+ {earnings.payouts.map((payout) => (
732
+ <div key={payout.id} className="p-4 flex items-center justify-between">
733
+ <div className="flex items-center gap-4">
734
+ <div
735
+ className={`w-10 h-10 rounded-full flex items-center justify-center ${
736
+ payout.status === "completed"
737
+ ? "bg-green-500/20"
738
+ : payout.status === "processing"
739
+ ? "bg-yellow-500/20"
740
+ : payout.status === "pending"
741
+ ? "bg-blue-500/20"
742
+ : "bg-red-500/20"
743
+ }`}
744
+ >
745
+ {payout.status === "completed" ? (
746
+ <Check className="w-5 h-5 text-green-500" />
747
+ ) : payout.status === "processing" ? (
748
+ <RefreshCw className="w-5 h-5 text-yellow-600 animate-spin" />
749
+ ) : payout.status === "pending" ? (
750
+ <Clock className="w-5 h-5 text-blue-500" />
751
+ ) : (
752
+ <AlertCircle className="w-5 h-5 text-red-500" />
753
+ )}
754
+ </div>
755
+ <div>
756
+ <p className="font-medium">${payout.amount.toFixed(2)}</p>
757
+ <p className="text-sm text-text-muted">
758
+ {new Date(payout.periodStart).toLocaleDateString()} -{" "}
759
+ {new Date(payout.periodEnd).toLocaleDateString()}
760
+ </p>
761
+ </div>
762
+ </div>
763
+ <div className="text-right">
764
+ <span
765
+ className={`px-2 py-1 rounded-full text-xs font-medium capitalize ${
766
+ payout.status === "completed"
767
+ ? "bg-green-500/20 text-green-500"
768
+ : payout.status === "processing"
769
+ ? "bg-yellow-500/20 text-yellow-600"
770
+ : payout.status === "pending"
771
+ ? "bg-blue-500/20 text-blue-500"
772
+ : "bg-red-500/20 text-red-500"
773
+ }`}
774
+ >
775
+ {payout.status}
776
+ </span>
777
+ {payout.completedAt && (
778
+ <p className="text-xs text-text-muted mt-1">
779
+ {new Date(payout.completedAt).toLocaleDateString()}
780
+ </p>
781
+ )}
782
+ </div>
783
+ </div>
784
+ ))}
785
+ </div>
786
+ ) : (
787
+ <div className="p-12 text-center text-text-muted">
788
+ <p>No payouts yet</p>
789
+ </div>
790
+ )}
791
+ </div>
792
+ </div>
793
+ );
794
+ }