@nordsym/apiclaw 1.0.0 → 1.1.0

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 (154) hide show
  1. package/AGENTS.md +74 -0
  2. package/HEARTBEAT.md +4 -0
  3. package/IDENTITY.md +22 -0
  4. package/README.md +197 -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 +6 -6
  10. package/convex/credits.ts +211 -0
  11. package/convex/http.ts +490 -0
  12. package/convex/providers.ts +516 -0
  13. package/convex/purchases.ts +183 -0
  14. package/convex/schema.ts +180 -0
  15. package/convex.json +3 -0
  16. package/dist/credentials.d.ts +19 -0
  17. package/dist/credentials.d.ts.map +1 -0
  18. package/dist/credentials.js +158 -0
  19. package/dist/credentials.js.map +1 -0
  20. package/dist/credits.d.ts +14 -11
  21. package/dist/credits.d.ts.map +1 -1
  22. package/dist/credits.js +151 -99
  23. package/dist/credits.js.map +1 -1
  24. package/dist/discovery.d.ts +7 -16
  25. package/dist/discovery.d.ts.map +1 -1
  26. package/dist/discovery.js +33 -40
  27. package/dist/discovery.js.map +1 -1
  28. package/dist/execute.d.ts +19 -0
  29. package/dist/execute.d.ts.map +1 -0
  30. package/dist/execute.js +285 -0
  31. package/dist/execute.js.map +1 -0
  32. package/dist/index.js +106 -30
  33. package/dist/index.js.map +1 -1
  34. package/dist/proxy.d.ts +6 -0
  35. package/dist/proxy.d.ts.map +1 -0
  36. package/dist/proxy.js +19 -0
  37. package/dist/proxy.js.map +1 -0
  38. package/dist/registry/apis.json +95362 -202
  39. package/dist/registry/apis_expanded.json +100853 -0
  40. package/dist/stripe.d.ts +68 -0
  41. package/dist/stripe.d.ts.map +1 -0
  42. package/dist/stripe.js +196 -0
  43. package/dist/stripe.js.map +1 -0
  44. package/dist/test.d.ts +3 -2
  45. package/dist/test.d.ts.map +1 -1
  46. package/dist/test.js +105 -75
  47. package/dist/test.js.map +1 -1
  48. package/dist/types.d.ts +0 -28
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/webhook.d.ts +2 -0
  51. package/dist/webhook.d.ts.map +1 -0
  52. package/dist/webhook.js +90 -0
  53. package/dist/webhook.js.map +1 -0
  54. package/landing/DESIGN.md +343 -0
  55. package/landing/package-lock.json +1190 -40
  56. package/landing/package.json +5 -2
  57. package/landing/public/android-chrome-192x192.png +0 -0
  58. package/landing/public/android-chrome-512x512.png +0 -0
  59. package/landing/public/apple-touch-icon.png +0 -0
  60. package/landing/public/demo.gif +0 -0
  61. package/landing/public/demo.mp4 +0 -0
  62. package/landing/public/favicon-16x16.png +0 -0
  63. package/landing/public/favicon-32x32.png +0 -0
  64. package/landing/public/favicon.ico +0 -0
  65. package/landing/public/favicon.svg +3 -0
  66. package/landing/public/icon.svg +47 -0
  67. package/landing/public/logo-mono.svg +37 -0
  68. package/landing/public/logo-simple.svg +45 -0
  69. package/landing/public/logo.svg +84 -0
  70. package/landing/public/og-image.png +0 -0
  71. package/landing/public/og-template.html +184 -0
  72. package/landing/public/site.webmanifest +31 -0
  73. package/landing/scripts/generate-assets.js +284 -0
  74. package/landing/scripts/generate-pngs.js +48 -0
  75. package/landing/scripts/generate-stats.js +42 -0
  76. package/landing/src/app/admin/page.tsx +348 -0
  77. package/landing/src/app/api/auth/magic-link/route.ts +73 -0
  78. package/landing/src/app/api/auth/session/route.ts +38 -0
  79. package/landing/src/app/api/auth/verify/route.ts +43 -0
  80. package/landing/src/app/api/og/route.tsx +74 -0
  81. package/landing/src/app/globals.css +439 -100
  82. package/landing/src/app/layout.tsx +37 -9
  83. package/landing/src/app/page.tsx +640 -552
  84. package/landing/src/app/providers/dashboard/login/page.tsx +176 -0
  85. package/landing/src/app/providers/dashboard/page.tsx +589 -0
  86. package/landing/src/app/providers/dashboard/verify/page.tsx +106 -0
  87. package/landing/src/app/providers/layout.tsx +14 -0
  88. package/landing/src/app/providers/page.tsx +402 -0
  89. package/landing/src/app/providers/register/page.tsx +670 -0
  90. package/landing/src/components/ProviderDashboard.tsx +794 -0
  91. package/landing/src/hooks/useDashboardData.ts +99 -0
  92. package/landing/src/lib/apis.json +116054 -0
  93. package/landing/src/lib/convex-client.ts +106 -0
  94. package/landing/src/lib/mock-data.ts +285 -0
  95. package/landing/src/lib/stats.json +6 -0
  96. package/landing/tailwind.config.ts +12 -11
  97. package/landing/tsconfig.tsbuildinfo +1 -0
  98. package/package.json +21 -20
  99. package/scripts/SYMBOT-FIX.md +238 -0
  100. package/scripts/demo-simulation.py +177 -0
  101. package/scripts/expand-more.py +502 -0
  102. package/scripts/expand-registry.py +434 -0
  103. package/scripts/history-sanitizer.ts +272 -0
  104. package/scripts/mass-scrape.py +1308 -0
  105. package/scripts/sync-and-deploy.sh +36 -0
  106. package/src/credentials.ts +177 -0
  107. package/src/credits.ts +190 -122
  108. package/src/discovery.ts +45 -58
  109. package/src/execute.ts +350 -0
  110. package/src/index.ts +113 -31
  111. package/src/proxy.ts +24 -0
  112. package/src/registry/apis.json +95362 -202
  113. package/src/registry/apis_expanded.json +100853 -0
  114. package/src/stripe.ts +243 -0
  115. package/src/test.ts +127 -89
  116. package/src/types.ts +0 -34
  117. package/src/webhook.ts +107 -0
  118. package/.github/ISSUE_TEMPLATE/add-api.yml +0 -123
  119. package/BRIEFING.md +0 -30
  120. package/backend/convex/apiKeys.ts +0 -75
  121. package/backend/convex/purchases.ts +0 -74
  122. package/backend/convex/schema.ts +0 -45
  123. package/backend/convex/transactions.ts +0 -57
  124. package/backend/convex/users.ts +0 -94
  125. package/backend/package-lock.json +0 -521
  126. package/backend/package.json +0 -15
  127. package/dist/registry/parse_apis.py +0 -146
  128. package/dist/revenuecat.d.ts +0 -61
  129. package/dist/revenuecat.d.ts.map +0 -1
  130. package/dist/revenuecat.js +0 -166
  131. package/dist/revenuecat.js.map +0 -1
  132. package/dist/webhooks/revenuecat.d.ts +0 -48
  133. package/dist/webhooks/revenuecat.d.ts.map +0 -1
  134. package/dist/webhooks/revenuecat.js +0 -119
  135. package/dist/webhooks/revenuecat.js.map +0 -1
  136. package/docs/revenuecat-setup.md +0 -89
  137. package/landing/src/app/api/keys/route.ts +0 -71
  138. package/landing/src/app/api/log/route.ts +0 -37
  139. package/landing/src/app/api/stats/route.ts +0 -37
  140. package/landing/src/app/page.tsx.bak +0 -567
  141. package/landing/src/components/AddKeyModal.tsx +0 -159
  142. package/newsletter-template.html +0 -71
  143. package/outreach/OUTREACH-SYSTEM.md +0 -211
  144. package/outreach/email-template.html +0 -179
  145. package/outreach/targets.md +0 -133
  146. package/src/registry/parse_apis.py +0 -146
  147. package/src/revenuecat.ts +0 -239
  148. package/src/webhooks/revenuecat.ts +0 -187
  149. /package/{backend/convex → convex}/README.md +0 -0
  150. /package/{backend/convex → convex}/_generated/api.js +0 -0
  151. /package/{backend/convex → convex}/_generated/dataModel.d.ts +0 -0
  152. /package/{backend/convex → convex}/_generated/server.d.ts +0 -0
  153. /package/{backend/convex → convex}/_generated/server.js +0 -0
  154. /package/{backend/convex → convex}/tsconfig.json +0 -0
@@ -0,0 +1,670 @@
1
+ "use client";
2
+
3
+ import {
4
+ ArrowRight, ArrowLeft, Check, Loader2, Sun, Moon, Github, AlertCircle,
5
+ FileJson, Globe, DollarSign, Tag, Sparkles, ExternalLink
6
+ } from "lucide-react";
7
+ import { useState, useEffect } from "react";
8
+ import Link from "next/link";
9
+
10
+ const categories = [
11
+ "SMS & Messaging",
12
+ "Email",
13
+ "Search",
14
+ "AI & LLM",
15
+ "Voice & TTS",
16
+ "Crypto & Blockchain",
17
+ "Market Data",
18
+ "Payments",
19
+ "Authentication",
20
+ "Storage & Files",
21
+ "Analytics",
22
+ "Social Media",
23
+ "Weather",
24
+ "Maps & Location",
25
+ "Translation",
26
+ "OCR & Vision",
27
+ "News",
28
+ "E-commerce",
29
+ "Other"
30
+ ];
31
+
32
+ const pricingModels = [
33
+ { value: "free", label: "Free", description: "Completely free to use" },
34
+ { value: "freemium", label: "Freemium", description: "Free tier + paid plans" },
35
+ { value: "paid", label: "Paid", description: "Paid only (trial available)" },
36
+ ];
37
+
38
+ type FormData = {
39
+ // Step 1: Provider Info
40
+ providerName: string;
41
+ email: string;
42
+ website: string;
43
+
44
+ // Step 2: API Details
45
+ apiName: string;
46
+ description: string;
47
+ category: string;
48
+
49
+ // Step 3: Technical
50
+ openApiUrl: string;
51
+ docsUrl: string;
52
+
53
+ // Step 4: Pricing
54
+ pricingModel: string;
55
+ pricingNotes: string;
56
+ };
57
+
58
+ export default function RegisterPage() {
59
+ const [isDark, setIsDark] = useState(false);
60
+ const [step, setStep] = useState(1);
61
+ const [isSubmitting, setIsSubmitting] = useState(false);
62
+ const [isComplete, setIsComplete] = useState(false);
63
+ const [parseStatus, setParseStatus] = useState<'idle' | 'parsing' | 'success' | 'error'>('idle');
64
+ const [error, setError] = useState<string | null>(null);
65
+
66
+ const [formData, setFormData] = useState<FormData>({
67
+ providerName: "",
68
+ email: "",
69
+ website: "",
70
+ apiName: "",
71
+ description: "",
72
+ category: "",
73
+ openApiUrl: "",
74
+ docsUrl: "",
75
+ pricingModel: "freemium",
76
+ pricingNotes: "",
77
+ });
78
+
79
+ useEffect(() => {
80
+ const saved = localStorage.getItem('theme');
81
+ const prefersDark = saved ? saved === 'dark' : true;
82
+ setIsDark(prefersDark);
83
+ document.documentElement.classList.toggle('dark', prefersDark);
84
+ }, []);
85
+
86
+ const toggleTheme = () => {
87
+ const newTheme = !isDark;
88
+ setIsDark(newTheme);
89
+ document.documentElement.classList.toggle('dark', newTheme);
90
+ localStorage.setItem('theme', newTheme ? 'dark' : 'light');
91
+ };
92
+
93
+ const updateField = (field: keyof FormData, value: string) => {
94
+ setFormData(prev => ({ ...prev, [field]: value }));
95
+ setError(null);
96
+ };
97
+
98
+ const parseOpenApiSpec = async () => {
99
+ if (!formData.openApiUrl) return;
100
+
101
+ setParseStatus('parsing');
102
+ try {
103
+ const response = await fetch(formData.openApiUrl);
104
+ if (!response.ok) throw new Error('Could not fetch spec');
105
+
106
+ const spec = await response.json();
107
+
108
+ // Extract info from OpenAPI spec
109
+ if (spec.info) {
110
+ if (spec.info.title && !formData.apiName) {
111
+ updateField('apiName', spec.info.title);
112
+ }
113
+ if (spec.info.description && !formData.description) {
114
+ updateField('description', spec.info.description.slice(0, 500));
115
+ }
116
+ }
117
+
118
+ setParseStatus('success');
119
+ setTimeout(() => setParseStatus('idle'), 3000);
120
+ } catch {
121
+ setParseStatus('error');
122
+ setTimeout(() => setParseStatus('idle'), 3000);
123
+ }
124
+ };
125
+
126
+ const validateStep = (stepNum: number): boolean => {
127
+ switch (stepNum) {
128
+ case 1:
129
+ if (!formData.providerName.trim()) {
130
+ setError("Provider name is required");
131
+ return false;
132
+ }
133
+ if (!formData.email.trim() || !formData.email.includes('@')) {
134
+ setError("Valid email is required");
135
+ return false;
136
+ }
137
+ return true;
138
+ case 2:
139
+ if (!formData.apiName.trim()) {
140
+ setError("API name is required");
141
+ return false;
142
+ }
143
+ if (!formData.description.trim()) {
144
+ setError("Description is required");
145
+ return false;
146
+ }
147
+ if (!formData.category) {
148
+ setError("Please select a category");
149
+ return false;
150
+ }
151
+ return true;
152
+ case 3:
153
+ // OpenAPI URL is optional
154
+ return true;
155
+ case 4:
156
+ return true;
157
+ default:
158
+ return true;
159
+ }
160
+ };
161
+
162
+ const nextStep = () => {
163
+ if (validateStep(step)) {
164
+ setStep(prev => Math.min(prev + 1, 4));
165
+ }
166
+ };
167
+
168
+ const prevStep = () => {
169
+ setStep(prev => Math.max(prev - 1, 1));
170
+ setError(null);
171
+ };
172
+
173
+ const handleSubmit = async () => {
174
+ setIsSubmitting(true);
175
+ setError(null);
176
+
177
+ try {
178
+ // Submit to Convex
179
+ const response = await fetch(`${process.env.NEXT_PUBLIC_CONVEX_URL || 'https://brilliant-puffin-712.eu-west-1.convex.cloud'}/api/mutation`, {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({
183
+ path: 'providers:registerProvider',
184
+ args: {
185
+ provider: {
186
+ name: formData.providerName,
187
+ email: formData.email,
188
+ website: formData.website,
189
+ },
190
+ api: {
191
+ name: formData.apiName,
192
+ description: formData.description,
193
+ category: formData.category,
194
+ openApiUrl: formData.openApiUrl || undefined,
195
+ docsUrl: formData.docsUrl || undefined,
196
+ pricingModel: formData.pricingModel,
197
+ pricingNotes: formData.pricingNotes || undefined,
198
+ }
199
+ }
200
+ })
201
+ });
202
+
203
+ if (!response.ok) {
204
+ const errorData = await response.json().catch(() => ({}));
205
+ throw new Error(errorData.message || 'Submission failed');
206
+ }
207
+
208
+ // Send confirmation email via Symbot SMTP
209
+ await fetch('https://nordsym.app.n8n.cloud/webhook/symbot-gmail', {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: JSON.stringify({
213
+ action: 'smtp',
214
+ to: formData.email,
215
+ subject: `🦞 Your API "${formData.apiName}" is now listed on APIClaw`,
216
+ message: `
217
+ <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto; padding: 40px 20px;">
218
+ <div style="text-align: center; margin-bottom: 32px;">
219
+ <span style="font-size: 48px;">🦞</span>
220
+ <h1 style="margin: 16px 0 8px; font-size: 24px; font-weight: 700;">Welcome to APIClaw!</h1>
221
+ </div>
222
+
223
+ <p style="color: #525252; font-size: 16px; line-height: 1.6;">
224
+ Hey there,
225
+ </p>
226
+
227
+ <p style="color: #525252; font-size: 16px; line-height: 1.6;">
228
+ <strong>${formData.apiName}</strong> has been submitted to APIClaw and is now discoverable by AI agents.
229
+ </p>
230
+
231
+ <div style="background: #f5f5f5; border-radius: 12px; padding: 24px; margin: 24px 0;">
232
+ <h3 style="margin: 0 0 16px; font-size: 16px;">What's next?</h3>
233
+ <ul style="color: #525252; margin: 0; padding-left: 20px;">
234
+ <li style="margin-bottom: 8px;">Your API is listed in our registry</li>
235
+ <li style="margin-bottom: 8px;">Agents can now discover it via MCP</li>
236
+ <li>We'll notify you when agents start finding your API</li>
237
+ </ul>
238
+ </div>
239
+
240
+ <p style="color: #525252; font-size: 16px; line-height: 1.6;">
241
+ Questions? Reply to this email or reach out on <a href="https://twitter.com/nordsym" style="color: #ef4444;">Twitter</a>.
242
+ </p>
243
+
244
+ <p style="color: #737373; font-size: 14px; margin-top: 32px;">
245
+ — The APIClaw Team<br>
246
+ <a href="https://apiclaw.nordsym.com" style="color: #ef4444;">apiclaw.nordsym.com</a>
247
+ </p>
248
+ </div>
249
+ `
250
+ })
251
+ }).catch(() => {
252
+ // Email failure shouldn't block success
253
+ console.log('Email notification failed, but registration succeeded');
254
+ });
255
+
256
+ setIsComplete(true);
257
+ } catch (err) {
258
+ setError(err instanceof Error ? err.message : 'Something went wrong. Please try again.');
259
+ } finally {
260
+ setIsSubmitting(false);
261
+ }
262
+ };
263
+
264
+ // Success screen
265
+ if (isComplete) {
266
+ return (
267
+ <main className="min-h-screen flex items-center justify-center px-6">
268
+ <div className="max-w-md w-full text-center">
269
+ <div className="w-20 h-20 rounded-full bg-accent/20 flex items-center justify-center mx-auto mb-8">
270
+ <Check className="w-10 h-10 text-accent" />
271
+ </div>
272
+ <h1 className="text-3xl font-bold mb-4">You&apos;re Listed! 🦞</h1>
273
+ <p className="text-text-secondary mb-8">
274
+ <strong>{formData.apiName}</strong> is now discoverable by AI agents on APIClaw.
275
+ We&apos;ve sent a confirmation to <strong>{formData.email}</strong>.
276
+ </p>
277
+ <div className="space-y-4">
278
+ <Link href="/" className="btn-primary w-full justify-center">
279
+ <Sparkles className="w-5 h-5" />
280
+ Explore APIClaw
281
+ </Link>
282
+ <Link href="/providers" className="btn-secondary w-full justify-center">
283
+ Back to Provider Hub
284
+ </Link>
285
+ </div>
286
+ </div>
287
+ </main>
288
+ );
289
+ }
290
+
291
+ return (
292
+ <main className="min-h-screen">
293
+ {/* Header */}
294
+ <header className="fixed top-0 w-full z-50 bg-background/90 backdrop-blur-xl border-b border-border-subtle">
295
+ <div className="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
296
+ <Link href="/" className="flex items-center gap-3">
297
+ <div className="w-9 h-9 rounded-xl bg-accent/20 flex items-center justify-center text-xl">
298
+ 🦞
299
+ </div>
300
+ <span className="font-bold text-lg tracking-tight">APIClaw</span>
301
+ </Link>
302
+ <div className="flex items-center gap-3">
303
+ <button
304
+ onClick={toggleTheme}
305
+ className="p-2 rounded-lg hover:bg-[var(--surface)] transition"
306
+ aria-label="Toggle theme"
307
+ >
308
+ {isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
309
+ </button>
310
+ <a
311
+ href="https://github.com/nordsym/apiclaw"
312
+ target="_blank"
313
+ rel="noopener noreferrer"
314
+ className="btn-secondary !py-2 !px-4 text-sm"
315
+ >
316
+ <Github className="w-4 h-4" />
317
+ </a>
318
+ </div>
319
+ </div>
320
+ </header>
321
+
322
+ <div className="pt-32 pb-24 px-6">
323
+ <div className="max-w-xl mx-auto">
324
+ {/* Progress */}
325
+ <div className="mb-12">
326
+ <div className="flex items-center justify-between mb-4">
327
+ {[1, 2, 3, 4].map((s) => (
328
+ <div key={s} className="flex items-center">
329
+ <div className={`w-10 h-10 rounded-full flex items-center justify-center font-semibold transition-all ${
330
+ s < step ? 'bg-accent text-background' :
331
+ s === step ? 'bg-accent/20 text-accent border-2 border-accent' :
332
+ 'bg-surface border border-border text-text-muted'
333
+ }`}>
334
+ {s < step ? <Check className="w-5 h-5" /> : s}
335
+ </div>
336
+ {s < 4 && (
337
+ <div className={`w-16 md:w-24 h-1 mx-2 rounded ${
338
+ s < step ? 'bg-accent' : 'bg-border'
339
+ }`} />
340
+ )}
341
+ </div>
342
+ ))}
343
+ </div>
344
+ <div className="text-center">
345
+ <span className="text-sm text-text-muted">
346
+ Step {step} of 4: {
347
+ step === 1 ? "Provider Info" :
348
+ step === 2 ? "API Details" :
349
+ step === 3 ? "Technical" :
350
+ "Pricing"
351
+ }
352
+ </span>
353
+ </div>
354
+ </div>
355
+
356
+ {/* Form */}
357
+ <div className="rounded-2xl bg-surface-elevated border border-border p-8">
358
+ {/* Step 1: Provider Info */}
359
+ {step === 1 && (
360
+ <div className="space-y-6">
361
+ <div className="text-center mb-8">
362
+ <h2 className="text-2xl font-bold mb-2">Tell us about you</h2>
363
+ <p className="text-text-secondary">Who&apos;s listing this API?</p>
364
+ </div>
365
+
366
+ <div>
367
+ <label className="block text-sm font-medium mb-2">Company / Provider Name *</label>
368
+ <input
369
+ type="text"
370
+ value={formData.providerName}
371
+ onChange={(e) => updateField('providerName', e.target.value)}
372
+ placeholder="Acme Inc"
373
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
374
+ />
375
+ </div>
376
+
377
+ <div>
378
+ <label className="block text-sm font-medium mb-2">Email *</label>
379
+ <input
380
+ type="email"
381
+ value={formData.email}
382
+ onChange={(e) => updateField('email', e.target.value)}
383
+ placeholder="you@company.com"
384
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
385
+ />
386
+ </div>
387
+
388
+ <div>
389
+ <label className="block text-sm font-medium mb-2">Website</label>
390
+ <input
391
+ type="url"
392
+ value={formData.website}
393
+ onChange={(e) => updateField('website', e.target.value)}
394
+ placeholder="https://company.com"
395
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
396
+ />
397
+ </div>
398
+ </div>
399
+ )}
400
+
401
+ {/* Step 2: API Details */}
402
+ {step === 2 && (
403
+ <div className="space-y-6">
404
+ <div className="text-center mb-8">
405
+ <h2 className="text-2xl font-bold mb-2">API Details</h2>
406
+ <p className="text-text-secondary">Help agents understand your API</p>
407
+ </div>
408
+
409
+ <div>
410
+ <label className="block text-sm font-medium mb-2">API Name *</label>
411
+ <input
412
+ type="text"
413
+ value={formData.apiName}
414
+ onChange={(e) => updateField('apiName', e.target.value)}
415
+ placeholder="My Awesome API"
416
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
417
+ />
418
+ </div>
419
+
420
+ <div>
421
+ <label className="block text-sm font-medium mb-2">Description *</label>
422
+ <textarea
423
+ value={formData.description}
424
+ onChange={(e) => updateField('description', e.target.value)}
425
+ placeholder="What does your API do? What problems does it solve?"
426
+ rows={4}
427
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition resize-none"
428
+ />
429
+ <p className="text-xs text-text-muted mt-1">{formData.description.length}/500 characters</p>
430
+ </div>
431
+
432
+ <div>
433
+ <label className="block text-sm font-medium mb-2">Category *</label>
434
+ <div className="relative">
435
+ <Tag className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
436
+ <select
437
+ value={formData.category}
438
+ onChange={(e) => updateField('category', e.target.value)}
439
+ className="w-full pl-12 pr-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition appearance-none cursor-pointer"
440
+ >
441
+ <option value="">Select a category...</option>
442
+ {categories.map((cat) => (
443
+ <option key={cat} value={cat}>{cat}</option>
444
+ ))}
445
+ </select>
446
+ </div>
447
+ </div>
448
+ </div>
449
+ )}
450
+
451
+ {/* Step 3: Technical */}
452
+ {step === 3 && (
453
+ <div className="space-y-6">
454
+ <div className="text-center mb-8">
455
+ <h2 className="text-2xl font-bold mb-2">Technical Details</h2>
456
+ <p className="text-text-secondary">Optional but recommended</p>
457
+ </div>
458
+
459
+ <div>
460
+ <label className="block text-sm font-medium mb-2">
461
+ OpenAPI Spec URL
462
+ <span className="text-text-muted font-normal ml-2">(optional)</span>
463
+ </label>
464
+ <div className="flex gap-2">
465
+ <div className="relative flex-1">
466
+ <FileJson className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
467
+ <input
468
+ type="url"
469
+ value={formData.openApiUrl}
470
+ onChange={(e) => updateField('openApiUrl', e.target.value)}
471
+ placeholder="https://api.example.com/openapi.json"
472
+ className="w-full pl-12 pr-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
473
+ />
474
+ </div>
475
+ <button
476
+ onClick={parseOpenApiSpec}
477
+ disabled={!formData.openApiUrl || parseStatus === 'parsing'}
478
+ className="btn-secondary !px-4 whitespace-nowrap disabled:opacity-50"
479
+ >
480
+ {parseStatus === 'parsing' ? (
481
+ <Loader2 className="w-5 h-5 animate-spin" />
482
+ ) : parseStatus === 'success' ? (
483
+ <Check className="w-5 h-5 text-green-500" />
484
+ ) : parseStatus === 'error' ? (
485
+ <AlertCircle className="w-5 h-5 text-red-500" />
486
+ ) : (
487
+ 'Parse'
488
+ )}
489
+ </button>
490
+ </div>
491
+ <p className="text-xs text-text-muted mt-1">
492
+ We&apos;ll auto-fill API details if you provide a spec
493
+ </p>
494
+ </div>
495
+
496
+ <div>
497
+ <label className="block text-sm font-medium mb-2">
498
+ Documentation URL
499
+ <span className="text-text-muted font-normal ml-2">(optional)</span>
500
+ </label>
501
+ <div className="relative">
502
+ <Globe className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
503
+ <input
504
+ type="url"
505
+ value={formData.docsUrl}
506
+ onChange={(e) => updateField('docsUrl', e.target.value)}
507
+ placeholder="https://docs.example.com"
508
+ className="w-full pl-12 pr-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
509
+ />
510
+ </div>
511
+ </div>
512
+
513
+ <div className="bg-surface rounded-xl p-4 border border-border">
514
+ <p className="text-sm text-text-secondary">
515
+ <strong className="text-text-primary">💡 Pro tip:</strong> APIs with OpenAPI specs get better placement in agent search results.
516
+ </p>
517
+ </div>
518
+ </div>
519
+ )}
520
+
521
+ {/* Step 4: Pricing */}
522
+ {step === 4 && (
523
+ <div className="space-y-6">
524
+ <div className="text-center mb-8">
525
+ <h2 className="text-2xl font-bold mb-2">Pricing Model</h2>
526
+ <p className="text-text-secondary">How do you charge for your API?</p>
527
+ </div>
528
+
529
+ <div className="space-y-3">
530
+ {pricingModels.map((model) => (
531
+ <label
532
+ key={model.value}
533
+ className={`block p-4 rounded-xl border cursor-pointer transition ${
534
+ formData.pricingModel === model.value
535
+ ? 'border-accent bg-accent/5'
536
+ : 'border-border hover:border-accent/50'
537
+ }`}
538
+ >
539
+ <div className="flex items-center gap-3">
540
+ <input
541
+ type="radio"
542
+ name="pricingModel"
543
+ value={model.value}
544
+ checked={formData.pricingModel === model.value}
545
+ onChange={(e) => updateField('pricingModel', e.target.value)}
546
+ className="sr-only"
547
+ />
548
+ <div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
549
+ formData.pricingModel === model.value
550
+ ? 'border-accent'
551
+ : 'border-border'
552
+ }`}>
553
+ {formData.pricingModel === model.value && (
554
+ <div className="w-3 h-3 rounded-full bg-accent" />
555
+ )}
556
+ </div>
557
+ <div className="flex-1">
558
+ <div className="font-medium">{model.label}</div>
559
+ <div className="text-sm text-text-muted">{model.description}</div>
560
+ </div>
561
+ <DollarSign className={`w-5 h-5 ${
562
+ formData.pricingModel === model.value ? 'text-accent' : 'text-text-muted'
563
+ }`} />
564
+ </div>
565
+ </label>
566
+ ))}
567
+ </div>
568
+
569
+ <div>
570
+ <label className="block text-sm font-medium mb-2">
571
+ Pricing Notes
572
+ <span className="text-text-muted font-normal ml-2">(optional)</span>
573
+ </label>
574
+ <textarea
575
+ value={formData.pricingNotes}
576
+ onChange={(e) => updateField('pricingNotes', e.target.value)}
577
+ placeholder="e.g., Free tier: 1000 requests/month. Pro: $29/month for unlimited."
578
+ rows={3}
579
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition resize-none"
580
+ />
581
+ </div>
582
+
583
+ {/* Summary */}
584
+ <div className="bg-surface rounded-xl p-6 border border-border">
585
+ <h3 className="font-semibold mb-4">Summary</h3>
586
+ <dl className="space-y-2 text-sm">
587
+ <div className="flex justify-between">
588
+ <dt className="text-text-muted">Provider:</dt>
589
+ <dd className="font-medium">{formData.providerName}</dd>
590
+ </div>
591
+ <div className="flex justify-between">
592
+ <dt className="text-text-muted">API:</dt>
593
+ <dd className="font-medium">{formData.apiName}</dd>
594
+ </div>
595
+ <div className="flex justify-between">
596
+ <dt className="text-text-muted">Category:</dt>
597
+ <dd className="font-medium">{formData.category}</dd>
598
+ </div>
599
+ <div className="flex justify-between">
600
+ <dt className="text-text-muted">Pricing:</dt>
601
+ <dd className="font-medium capitalize">{formData.pricingModel}</dd>
602
+ </div>
603
+ </dl>
604
+ </div>
605
+ </div>
606
+ )}
607
+
608
+ {/* Error */}
609
+ {error && (
610
+ <div className="mt-6 p-4 rounded-xl bg-red-500/10 border border-red-500/30 flex items-center gap-3 text-red-500">
611
+ <AlertCircle className="w-5 h-5 flex-shrink-0" />
612
+ <span className="text-sm">{error}</span>
613
+ </div>
614
+ )}
615
+
616
+ {/* Navigation */}
617
+ <div className="flex items-center justify-between mt-8 pt-6 border-t border-border">
618
+ <button
619
+ onClick={prevStep}
620
+ disabled={step === 1}
621
+ className="btn-secondary disabled:opacity-50 disabled:cursor-not-allowed"
622
+ >
623
+ <ArrowLeft className="w-4 h-4" />
624
+ Back
625
+ </button>
626
+
627
+ {step < 4 ? (
628
+ <button onClick={nextStep} className="btn-primary">
629
+ Continue
630
+ <ArrowRight className="w-4 h-4" />
631
+ </button>
632
+ ) : (
633
+ <button
634
+ onClick={handleSubmit}
635
+ disabled={isSubmitting}
636
+ className="btn-primary disabled:opacity-50"
637
+ >
638
+ {isSubmitting ? (
639
+ <>
640
+ <Loader2 className="w-5 h-5 animate-spin" />
641
+ Submitting...
642
+ </>
643
+ ) : (
644
+ <>
645
+ <Sparkles className="w-5 h-5" />
646
+ Submit API
647
+ </>
648
+ )}
649
+ </button>
650
+ )}
651
+ </div>
652
+ </div>
653
+
654
+ {/* Help text */}
655
+ <p className="text-center text-text-muted text-sm mt-6">
656
+ Questions?{' '}
657
+ <a href="mailto:gustav@nordsym.com" className="text-accent hover:underline">
658
+ Contact us
659
+ </a>
660
+ {' '}or check the{' '}
661
+ <Link href="/providers#faq" className="text-accent hover:underline inline-flex items-center gap-1">
662
+ FAQ
663
+ <ExternalLink className="w-3 h-3" />
664
+ </Link>
665
+ </p>
666
+ </div>
667
+ </div>
668
+ </main>
669
+ );
670
+ }