@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,348 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Shield, Users, Zap, TrendingUp, Check, X, Clock, ExternalLink, RefreshCw, Eye } from "lucide-react";
5
+ import Link from "next/link";
6
+
7
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
8
+
9
+ interface Stats {
10
+ totalProviders: number;
11
+ approvedProviders: number;
12
+ pendingProviders: number;
13
+ totalAPIs: number;
14
+ approvedAPIs: number;
15
+ pendingAPIs: number;
16
+ totalDiscoveries: number;
17
+ }
18
+
19
+ interface Provider {
20
+ _id: string;
21
+ name: string;
22
+ email: string;
23
+ company?: string;
24
+ website?: string;
25
+ status: string;
26
+ createdAt: number;
27
+ approvedAt?: number;
28
+ }
29
+
30
+ interface ProviderAPI {
31
+ _id: string;
32
+ providerId: string;
33
+ name: string;
34
+ description: string;
35
+ category: string;
36
+ status: string;
37
+ pricingModel: string;
38
+ discoveryCount?: number;
39
+ createdAt: number;
40
+ }
41
+
42
+ // Simple admin password check (set via env or hardcode for MVP)
43
+ const ADMIN_PASSWORD = "nordsym2026";
44
+
45
+ export default function AdminPage() {
46
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
47
+ const [password, setPassword] = useState("");
48
+ const [stats, setStats] = useState<Stats | null>(null);
49
+ const [providers, setProviders] = useState<Provider[]>([]);
50
+ const [apis, setApis] = useState<ProviderAPI[]>([]);
51
+ const [isLoading, setIsLoading] = useState(true);
52
+ const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
53
+
54
+ useEffect(() => {
55
+ // Check if already authenticated this session
56
+ const auth = sessionStorage.getItem("admin_auth");
57
+ if (auth === "true") {
58
+ setIsAuthenticated(true);
59
+ }
60
+ }, []);
61
+
62
+ useEffect(() => {
63
+ if (!isAuthenticated) return;
64
+ loadData();
65
+ }, [isAuthenticated]);
66
+
67
+ const handleLogin = (e: React.FormEvent) => {
68
+ e.preventDefault();
69
+ if (password === ADMIN_PASSWORD) {
70
+ setIsAuthenticated(true);
71
+ sessionStorage.setItem("admin_auth", "true");
72
+ } else {
73
+ alert("Invalid password");
74
+ }
75
+ };
76
+
77
+ const loadData = async () => {
78
+ setIsLoading(true);
79
+ try {
80
+ // Load stats
81
+ const statsRes = await fetch(`${CONVEX_URL}/api/query`, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({ path: "providers:getProviderStats", args: {} }),
85
+ });
86
+ const statsData = await statsRes.json();
87
+ setStats(statsData);
88
+
89
+ // Load all providers (using a simple query)
90
+ const providersRes = await fetch(`${CONVEX_URL}/api/query`, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({ path: "providers:getAllProviders", args: {} }),
94
+ });
95
+ const providersData = await providersRes.json();
96
+ setProviders(providersData || []);
97
+
98
+ // Load all APIs
99
+ const apisRes = await fetch(`${CONVEX_URL}/api/query`, {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify({ path: "providers:getAllAPIs", args: {} }),
103
+ });
104
+ const apisData = await apisRes.json();
105
+ setApis(apisData || []);
106
+ } catch (error) {
107
+ console.error("Failed to load admin data:", error);
108
+ } finally {
109
+ setIsLoading(false);
110
+ }
111
+ };
112
+
113
+ if (!isAuthenticated) {
114
+ return (
115
+ <main className="min-h-screen flex items-center justify-center px-6">
116
+ <div className="max-w-sm w-full">
117
+ <div className="text-center mb-8">
118
+ <Shield className="w-16 h-16 text-accent mx-auto mb-4" />
119
+ <h1 className="text-2xl font-bold">Admin Access</h1>
120
+ <p className="text-text-muted">Enter password to continue</p>
121
+ </div>
122
+ <form onSubmit={handleLogin} className="space-y-4">
123
+ <input
124
+ type="password"
125
+ value={password}
126
+ onChange={(e) => setPassword(e.target.value)}
127
+ placeholder="Password"
128
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none"
129
+ />
130
+ <button type="submit" className="btn-primary w-full justify-center">
131
+ Access Admin Panel
132
+ </button>
133
+ </form>
134
+ </div>
135
+ </main>
136
+ );
137
+ }
138
+
139
+ if (isLoading) {
140
+ return (
141
+ <main className="min-h-screen flex items-center justify-center">
142
+ <RefreshCw className="w-12 h-12 text-accent animate-spin" />
143
+ </main>
144
+ );
145
+ }
146
+
147
+ const providerAPIs = selectedProvider
148
+ ? apis.filter((a) => a.providerId === selectedProvider)
149
+ : [];
150
+
151
+ return (
152
+ <main className="min-h-screen bg-background">
153
+ {/* Header */}
154
+ <header className="border-b border-border bg-surface/50 backdrop-blur-xl sticky top-0 z-50">
155
+ <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
156
+ <div className="flex items-center gap-4">
157
+ <Link href="/" className="w-10 h-10 rounded-xl bg-accent/20 flex items-center justify-center text-xl">
158
+ 🦞
159
+ </Link>
160
+ <div>
161
+ <h1 className="font-bold text-lg">APIClaw Admin</h1>
162
+ <p className="text-sm text-text-muted">Provider Management</p>
163
+ </div>
164
+ </div>
165
+ <button onClick={loadData} className="btn-secondary !py-2 !px-4 text-sm">
166
+ <RefreshCw className="w-4 h-4" />
167
+ Refresh
168
+ </button>
169
+ </div>
170
+ </header>
171
+
172
+ <div className="max-w-7xl mx-auto px-6 py-8">
173
+ {/* Stats */}
174
+ {stats && (
175
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
176
+ <StatCard
177
+ icon={Users}
178
+ label="Providers"
179
+ value={stats.totalProviders}
180
+ subValue={`${stats.approvedProviders} approved`}
181
+ />
182
+ <StatCard
183
+ icon={Zap}
184
+ label="APIs"
185
+ value={stats.totalAPIs}
186
+ subValue={`${stats.approvedAPIs} approved`}
187
+ />
188
+ <StatCard
189
+ icon={TrendingUp}
190
+ label="Discoveries"
191
+ value={stats.totalDiscoveries}
192
+ />
193
+ <StatCard
194
+ icon={Clock}
195
+ label="Pending"
196
+ value={stats.pendingProviders + stats.pendingAPIs}
197
+ subValue="need review"
198
+ accent
199
+ />
200
+ </div>
201
+ )}
202
+
203
+ {/* Providers Table */}
204
+ <div className="bg-surface-elevated rounded-2xl border border-border overflow-hidden mb-8">
205
+ <div className="p-6 border-b border-border">
206
+ <h2 className="font-bold text-lg">All Providers</h2>
207
+ </div>
208
+ <div className="overflow-x-auto">
209
+ <table className="w-full">
210
+ <thead className="bg-surface">
211
+ <tr>
212
+ <th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Provider</th>
213
+ <th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Email</th>
214
+ <th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Status</th>
215
+ <th className="text-left px-6 py-3 text-sm font-medium text-text-muted">APIs</th>
216
+ <th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Registered</th>
217
+ <th className="text-left px-6 py-3 text-sm font-medium text-text-muted">Actions</th>
218
+ </tr>
219
+ </thead>
220
+ <tbody className="divide-y divide-border">
221
+ {providers.map((provider) => {
222
+ const apiCount = apis.filter((a) => a.providerId === provider._id).length;
223
+ return (
224
+ <tr key={provider._id} className="hover:bg-surface/50">
225
+ <td className="px-6 py-4">
226
+ <div>
227
+ <p className="font-medium">{provider.name}</p>
228
+ {provider.company && (
229
+ <p className="text-sm text-text-muted">{provider.company}</p>
230
+ )}
231
+ </div>
232
+ </td>
233
+ <td className="px-6 py-4 text-sm">{provider.email}</td>
234
+ <td className="px-6 py-4">
235
+ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
236
+ provider.status === "approved"
237
+ ? "bg-green-500/20 text-green-500"
238
+ : provider.status === "pending"
239
+ ? "bg-yellow-500/20 text-yellow-600"
240
+ : "bg-red-500/20 text-red-500"
241
+ }`}>
242
+ {provider.status}
243
+ </span>
244
+ </td>
245
+ <td className="px-6 py-4 text-sm">{apiCount}</td>
246
+ <td className="px-6 py-4 text-sm text-text-muted">
247
+ {new Date(provider.createdAt).toLocaleDateString()}
248
+ </td>
249
+ <td className="px-6 py-4">
250
+ <button
251
+ onClick={() => setSelectedProvider(
252
+ selectedProvider === provider._id ? null : provider._id
253
+ )}
254
+ className="p-2 rounded-lg hover:bg-surface transition"
255
+ title="View APIs"
256
+ >
257
+ <Eye className="w-4 h-4 text-text-muted" />
258
+ </button>
259
+ </td>
260
+ </tr>
261
+ );
262
+ })}
263
+ {providers.length === 0 && (
264
+ <tr>
265
+ <td colSpan={6} className="px-6 py-12 text-center text-text-muted">
266
+ No providers registered yet
267
+ </td>
268
+ </tr>
269
+ )}
270
+ </tbody>
271
+ </table>
272
+ </div>
273
+ </div>
274
+
275
+ {/* Selected Provider APIs */}
276
+ {selectedProvider && (
277
+ <div className="bg-surface-elevated rounded-2xl border border-border overflow-hidden">
278
+ <div className="p-6 border-b border-border flex items-center justify-between">
279
+ <h2 className="font-bold text-lg">
280
+ APIs for {providers.find((p) => p._id === selectedProvider)?.name}
281
+ </h2>
282
+ <button
283
+ onClick={() => setSelectedProvider(null)}
284
+ className="text-text-muted hover:text-text-primary"
285
+ >
286
+ <X className="w-5 h-5" />
287
+ </button>
288
+ </div>
289
+ <div className="divide-y divide-border">
290
+ {providerAPIs.map((api) => (
291
+ <div key={api._id} className="p-6">
292
+ <div className="flex items-start justify-between mb-2">
293
+ <div>
294
+ <h3 className="font-semibold">{api.name}</h3>
295
+ <p className="text-sm text-text-muted">{api.category} • {api.pricingModel}</p>
296
+ </div>
297
+ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
298
+ api.status === "approved"
299
+ ? "bg-green-500/20 text-green-500"
300
+ : "bg-yellow-500/20 text-yellow-600"
301
+ }`}>
302
+ {api.status}
303
+ </span>
304
+ </div>
305
+ <p className="text-text-secondary text-sm mb-3">{api.description}</p>
306
+ <div className="flex items-center gap-4 text-sm text-text-muted">
307
+ <span>{api.discoveryCount || 0} discoveries</span>
308
+ <span>Created {new Date(api.createdAt).toLocaleDateString()}</span>
309
+ </div>
310
+ </div>
311
+ ))}
312
+ {providerAPIs.length === 0 && (
313
+ <div className="p-12 text-center text-text-muted">
314
+ No APIs listed for this provider
315
+ </div>
316
+ )}
317
+ </div>
318
+ </div>
319
+ )}
320
+ </div>
321
+ </main>
322
+ );
323
+ }
324
+
325
+ function StatCard({
326
+ icon: Icon,
327
+ label,
328
+ value,
329
+ subValue,
330
+ accent,
331
+ }: {
332
+ icon: typeof Users;
333
+ label: string;
334
+ value: number;
335
+ subValue?: string;
336
+ accent?: boolean;
337
+ }) {
338
+ return (
339
+ <div className={`rounded-2xl border p-5 ${accent ? "bg-accent/10 border-accent/30" : "bg-surface-elevated border-border"}`}>
340
+ <div className="flex items-center gap-2 mb-2">
341
+ <Icon className={`w-5 h-5 ${accent ? "text-accent" : "text-text-muted"}`} />
342
+ <span className="text-sm text-text-muted">{label}</span>
343
+ </div>
344
+ <p className={`text-3xl font-bold ${accent ? "text-accent" : ""}`}>{value}</p>
345
+ {subValue && <p className="text-xs text-text-muted mt-1">{subValue}</p>}
346
+ </div>
347
+ );
348
+ }
@@ -0,0 +1,73 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const { email } = await req.json();
8
+
9
+ if (!email || !email.includes("@")) {
10
+ return NextResponse.json({ error: "Valid email required" }, { status: 400 });
11
+ }
12
+
13
+ // Create magic link in Convex
14
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json" },
17
+ body: JSON.stringify({
18
+ path: "providers:createMagicLink",
19
+ args: { email: email.toLowerCase() },
20
+ }),
21
+ });
22
+
23
+ if (!response.ok) {
24
+ throw new Error("Failed to create magic link");
25
+ }
26
+
27
+ const { token } = await response.json();
28
+
29
+ // Send magic link email via n8n
30
+ const magicLinkUrl = `${process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.nordsym.com"}/providers/dashboard/verify?token=${token}`;
31
+
32
+ await fetch("https://nordsym.app.n8n.cloud/webhook/symbot-gmail", {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify({
36
+ action: "smtp",
37
+ to: email,
38
+ subject: "🦞 Sign in to APIClaw Dashboard",
39
+ message: `
40
+ <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 500px; margin: 0 auto; padding: 40px 20px;">
41
+ <div style="text-align: center; margin-bottom: 32px;">
42
+ <span style="font-size: 48px;">🦞</span>
43
+ <h1 style="margin: 16px 0 8px; font-size: 24px; font-weight: 700;">Sign in to APIClaw</h1>
44
+ </div>
45
+
46
+ <p style="color: #525252; font-size: 16px; line-height: 1.6; text-align: center;">
47
+ Click the button below to sign in to your provider dashboard.
48
+ </p>
49
+
50
+ <div style="text-align: center; margin: 32px 0;">
51
+ <a href="${magicLinkUrl}" style="display: inline-block; background: #ef4444; color: white; text-decoration: none; padding: 14px 32px; border-radius: 8px; font-weight: 600; font-size: 16px;">
52
+ Sign In to Dashboard
53
+ </a>
54
+ </div>
55
+
56
+ <p style="color: #737373; font-size: 14px; text-align: center;">
57
+ This link expires in 15 minutes.
58
+ </p>
59
+
60
+ <p style="color: #737373; font-size: 12px; margin-top: 40px; text-align: center;">
61
+ If you didn't request this email, you can safely ignore it.
62
+ </p>
63
+ </div>
64
+ `,
65
+ }),
66
+ });
67
+
68
+ return NextResponse.json({ success: true });
69
+ } catch (error) {
70
+ console.error("Magic link error:", error);
71
+ return NextResponse.json({ error: "Failed to send magic link" }, { status: 500 });
72
+ }
73
+ }
@@ -0,0 +1,38 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const { token } = await req.json();
8
+
9
+ if (!token) {
10
+ return NextResponse.json({ error: "Token required" }, { status: 400 });
11
+ }
12
+
13
+ // Get session from Convex
14
+ const response = await fetch(`${CONVEX_URL}/api/query`, {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json" },
17
+ body: JSON.stringify({
18
+ path: "providers:getSession",
19
+ args: { token },
20
+ }),
21
+ });
22
+
23
+ if (!response.ok) {
24
+ throw new Error("Session check failed");
25
+ }
26
+
27
+ const session = await response.json();
28
+
29
+ if (!session) {
30
+ return NextResponse.json({ error: "Invalid session" }, { status: 401 });
31
+ }
32
+
33
+ return NextResponse.json({ session });
34
+ } catch (error) {
35
+ console.error("Session error:", error);
36
+ return NextResponse.json({ error: "Session check failed" }, { status: 500 });
37
+ }
38
+ }
@@ -0,0 +1,43 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const { token } = await req.json();
8
+
9
+ if (!token) {
10
+ return NextResponse.json({ error: "Token required" }, { status: 400 });
11
+ }
12
+
13
+ // Verify magic link in Convex
14
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json" },
17
+ body: JSON.stringify({
18
+ path: "providers:verifyMagicLink",
19
+ args: { token },
20
+ }),
21
+ });
22
+
23
+ if (!response.ok) {
24
+ throw new Error("Verification failed");
25
+ }
26
+
27
+ const result = await response.json();
28
+
29
+ if (!result.success) {
30
+ return NextResponse.json({ error: result.error || "Invalid token" }, { status: 400 });
31
+ }
32
+
33
+ // Return session token
34
+ return NextResponse.json({
35
+ success: true,
36
+ sessionToken: result.sessionToken,
37
+ provider: result.provider,
38
+ });
39
+ } catch (error) {
40
+ console.error("Verify error:", error);
41
+ return NextResponse.json({ error: "Verification failed" }, { status: 500 });
42
+ }
43
+ }
@@ -0,0 +1,84 @@
1
+ import { ImageResponse } from '@vercel/og';
2
+ import statsData from '@/lib/stats.json';
3
+
4
+ export const runtime = 'edge';
5
+
6
+ export async function GET() {
7
+ return new ImageResponse(
8
+ (
9
+ <div
10
+ style={{
11
+ height: '100%',
12
+ width: '100%',
13
+ display: 'flex',
14
+ flexDirection: 'column',
15
+ alignItems: 'center',
16
+ justifyContent: 'center',
17
+ backgroundColor: '#0d0d0d',
18
+ padding: '40px',
19
+ }}
20
+ >
21
+ <div style={{ fontSize: 120, marginBottom: 20 }}>🦞</div>
22
+ <div
23
+ style={{
24
+ fontSize: 72,
25
+ fontWeight: 'bold',
26
+ color: '#ef4444',
27
+ marginBottom: 20,
28
+ }}
29
+ >
30
+ APIClaw
31
+ </div>
32
+ <div
33
+ style={{
34
+ fontSize: 36,
35
+ color: '#ffffff',
36
+ textAlign: 'center',
37
+ maxWidth: 800,
38
+ }}
39
+ >
40
+ The API Layer for AI Agents
41
+ </div>
42
+ <div
43
+ style={{
44
+ fontSize: 24,
45
+ color: '#a3a3a3',
46
+ marginTop: 16,
47
+ textAlign: 'center',
48
+ }}
49
+ >
50
+ Find, evaluate, and integrate APIs in milliseconds.
51
+ </div>
52
+ <div
53
+ style={{
54
+ display: 'flex',
55
+ gap: 40,
56
+ marginTop: 40,
57
+ color: '#ef4444',
58
+ fontSize: 28,
59
+ }}
60
+ >
61
+ <span>{statsData.apiCount.toLocaleString()}+ APIs</span>
62
+ <span>•</span>
63
+ <span>{statsData.categoryCount}+ Categories</span>
64
+ <span>•</span>
65
+ <span>MCP Native</span>
66
+ </div>
67
+ <div
68
+ style={{
69
+ fontSize: 20,
70
+ color: '#737373',
71
+ marginTop: 24,
72
+ fontStyle: 'italic',
73
+ }}
74
+ >
75
+ Built for the agentic era
76
+ </div>
77
+ </div>
78
+ ),
79
+ {
80
+ width: 1200,
81
+ height: 630,
82
+ }
83
+ );
84
+ }