@nordsym/apiclaw 1.2.2 → 1.2.3

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 (77) hide show
  1. package/AGENTS.md +50 -33
  2. package/README.md +22 -12
  3. package/SOUL.md +60 -19
  4. package/STATUS.md +91 -169
  5. package/convex/_generated/api.d.ts +6 -0
  6. package/convex/directCall.ts +598 -0
  7. package/convex/providers.ts +341 -26
  8. package/convex/schema.ts +87 -0
  9. package/convex/usage.ts +260 -0
  10. package/convex/waitlist.ts +55 -0
  11. package/data/combined-02-26.json +22102 -0
  12. package/data/night-expansion-02-26-06-batch2.json +1898 -0
  13. package/data/night-expansion-02-26-06-batch3.json +1410 -0
  14. package/data/night-expansion-02-26-06.json +3146 -0
  15. package/data/night-expansion-02-26-full.json +9726 -0
  16. package/data/night-expansion-02-26-v2.json +330 -0
  17. package/data/night-expansion-02-26.json +171 -0
  18. package/dist/crypto.d.ts +7 -0
  19. package/dist/crypto.d.ts.map +1 -0
  20. package/dist/crypto.js +67 -0
  21. package/dist/crypto.js.map +1 -0
  22. package/dist/execute-dynamic.d.ts +116 -0
  23. package/dist/execute-dynamic.d.ts.map +1 -0
  24. package/dist/execute-dynamic.js +456 -0
  25. package/dist/execute-dynamic.js.map +1 -0
  26. package/dist/execute.d.ts +2 -1
  27. package/dist/execute.d.ts.map +1 -1
  28. package/dist/execute.js +35 -5
  29. package/dist/execute.js.map +1 -1
  30. package/dist/index.js +33 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/registry/apis.json +2081 -3
  33. package/docs/PRD-customer-key-passthrough.md +184 -0
  34. package/landing/public/badges/available-on-apiclaw.svg +14 -0
  35. package/landing/scripts/generate-stats.js +75 -4
  36. package/landing/src/app/admin/page.tsx +1 -1
  37. package/landing/src/app/api/auth/magic-link/route.ts +1 -1
  38. package/landing/src/app/api/auth/session/route.ts +1 -1
  39. package/landing/src/app/api/auth/verify/route.ts +1 -1
  40. package/landing/src/app/api/og/route.tsx +5 -3
  41. package/landing/src/app/docs/page.tsx +5 -4
  42. package/landing/src/app/earn/page.tsx +14 -11
  43. package/landing/src/app/globals.css +16 -15
  44. package/landing/src/app/layout.tsx +2 -2
  45. package/landing/src/app/page.tsx +425 -254
  46. package/landing/src/app/providers/dashboard/[apiId]/actions/[actionId]/edit/page.tsx +600 -0
  47. package/landing/src/app/providers/dashboard/[apiId]/actions/new/page.tsx +583 -0
  48. package/landing/src/app/providers/dashboard/[apiId]/actions/page.tsx +301 -0
  49. package/landing/src/app/providers/dashboard/[apiId]/direct-call/page.tsx +659 -0
  50. package/landing/src/app/providers/dashboard/[apiId]/page.tsx +381 -0
  51. package/landing/src/app/providers/dashboard/[apiId]/test/page.tsx +418 -0
  52. package/landing/src/app/providers/dashboard/layout.tsx +292 -0
  53. package/landing/src/app/providers/dashboard/page.tsx +353 -290
  54. package/landing/src/app/providers/register/page.tsx +87 -10
  55. package/landing/src/components/AiClientDropdown.tsx +85 -0
  56. package/landing/src/components/ConfigHelperModal.tsx +113 -0
  57. package/landing/src/components/HeroTabs.tsx +187 -0
  58. package/landing/src/components/ShareIntegrationModal.tsx +198 -0
  59. package/landing/src/hooks/useDashboardData.ts +53 -1
  60. package/landing/src/lib/apis.json +46554 -174
  61. package/landing/src/lib/convex-client.ts +22 -3
  62. package/landing/src/lib/stats.json +4 -4
  63. package/landing/tsconfig.tsbuildinfo +1 -1
  64. package/night-expansion-02-26-06-batch2.py +368 -0
  65. package/night-expansion-02-26-06-batch3.py +299 -0
  66. package/night-expansion-02-26-06.py +756 -0
  67. package/package.json +1 -1
  68. package/scripts/bulk-add-public-apis-v2.py +418 -0
  69. package/scripts/night-expansion-02-26-v2.py +296 -0
  70. package/scripts/night-expansion-02-26.py +890 -0
  71. package/scripts/seed-complete-api.js +181 -0
  72. package/scripts/seed-demo-api.sh +44 -0
  73. package/src/crypto.ts +75 -0
  74. package/src/execute-dynamic.ts +589 -0
  75. package/src/execute.ts +41 -5
  76. package/src/index.ts +38 -4
  77. package/src/registry/apis.json +2081 -3
@@ -0,0 +1,659 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useParams, useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import {
7
+ Save,
8
+ Loader2,
9
+ AlertCircle,
10
+ Check,
11
+ Eye,
12
+ EyeOff,
13
+ Info,
14
+ PlayCircle,
15
+ ChevronLeft,
16
+ Shield,
17
+ Zap,
18
+ Clock,
19
+ DollarSign,
20
+ Globe,
21
+ Key,
22
+ ArrowRight,
23
+ } from "lucide-react";
24
+ import { convexQuery, convexMutation, type ProviderAPI } from "@/lib/convex-client";
25
+ import { ShareIntegrationModal } from "@/components/ShareIntegrationModal";
26
+
27
+ interface DirectCallConfig {
28
+ _id?: string;
29
+ apiId: string;
30
+ baseUrl: string;
31
+ authType: "bearer" | "basic" | "api_key" | "none";
32
+ authHeader: string;
33
+ authPrefix: string;
34
+ masterApiKey: string;
35
+ rateLimitPerUser: number;
36
+ rateLimitPerDay: number;
37
+ pricePerRequest: number;
38
+ status: "draft" | "testing" | "live";
39
+ // Customer key passthrough
40
+ allowCustomerKeys: boolean;
41
+ requireCustomerKeys: boolean;
42
+ }
43
+
44
+ const authTypes = [
45
+ { value: "bearer", label: "Bearer Token", description: "Authorization: Bearer <token>" },
46
+ { value: "basic", label: "Basic Auth", description: "Authorization: Basic <base64>" },
47
+ { value: "api_key", label: "API Key Header", description: "Custom header with API key" },
48
+ { value: "none", label: "No Auth", description: "Public API, no authentication" },
49
+ ];
50
+
51
+ export default function DirectCallSetupPage() {
52
+ const params = useParams();
53
+ const router = useRouter();
54
+ const apiId = params.apiId as string;
55
+
56
+ const [api, setApi] = useState<ProviderAPI | null>(null);
57
+ const [isLoading, setIsLoading] = useState(true);
58
+ const [isSaving, setIsSaving] = useState(false);
59
+ const [error, setError] = useState<string | null>(null);
60
+ const [success, setSuccess] = useState<string | null>(null);
61
+ const [showApiKey, setShowApiKey] = useState(false);
62
+ const [urlError, setUrlError] = useState<string | null>(null);
63
+ const [showShareModal, setShowShareModal] = useState(false);
64
+ const [wasLiveBefore, setWasLiveBefore] = useState(false);
65
+
66
+ const [formData, setFormData] = useState<DirectCallConfig>({
67
+ apiId,
68
+ baseUrl: "",
69
+ authType: "bearer",
70
+ authHeader: "Authorization",
71
+ authPrefix: "Bearer",
72
+ masterApiKey: "",
73
+ rateLimitPerUser: 60,
74
+ rateLimitPerDay: 10000,
75
+ pricePerRequest: 1,
76
+ status: "draft",
77
+ allowCustomerKeys: true,
78
+ requireCustomerKeys: false,
79
+ });
80
+
81
+ useEffect(() => {
82
+ const loadData = async () => {
83
+ const token = localStorage.getItem("apiclaw_session");
84
+ if (!token) {
85
+ router.push("/providers/dashboard/login");
86
+ return;
87
+ }
88
+
89
+ try {
90
+ // Load API details
91
+ const apiData = await convexQuery<ProviderAPI | null>("providers:getApiById", { apiId });
92
+ if (!apiData) {
93
+ setError("API not found");
94
+ return;
95
+ }
96
+ setApi(apiData);
97
+
98
+ // Load existing Direct Call config if exists
99
+ try {
100
+ const existingConfig = await convexQuery<DirectCallConfig | null>("directCall:getConfig", { apiId });
101
+ if (existingConfig) {
102
+ setFormData({
103
+ ...existingConfig,
104
+ masterApiKey: "", // Never show existing key, placeholder only
105
+ });
106
+ // Track if already live (to not show modal on re-saves)
107
+ if (existingConfig.status === "live") {
108
+ setWasLiveBefore(true);
109
+ }
110
+ }
111
+ } catch {
112
+ // No existing config - that's fine
113
+ }
114
+ } catch (err) {
115
+ console.error("Failed to load data:", err);
116
+ setError(err instanceof Error ? err.message : "Failed to load data");
117
+ } finally {
118
+ setIsLoading(false);
119
+ }
120
+ };
121
+
122
+ loadData();
123
+ }, [apiId, router]);
124
+
125
+ const validateUrl = (url: string): boolean => {
126
+ if (!url) {
127
+ setUrlError("Base URL is required");
128
+ return false;
129
+ }
130
+
131
+ try {
132
+ const parsed = new URL(url);
133
+
134
+ // Must be HTTPS
135
+ if (parsed.protocol !== "https:") {
136
+ setUrlError("URL must use HTTPS");
137
+ return false;
138
+ }
139
+
140
+ // Block private IP ranges
141
+ const blockedPatterns = [
142
+ /^127\./,
143
+ /^10\./,
144
+ /^192\.168\./,
145
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
146
+ /^localhost$/i,
147
+ /^0\.0\.0\.0$/,
148
+ ];
149
+
150
+ if (blockedPatterns.some((p) => p.test(parsed.hostname))) {
151
+ setUrlError("Private/local URLs are not allowed");
152
+ return false;
153
+ }
154
+
155
+ setUrlError(null);
156
+ return true;
157
+ } catch {
158
+ setUrlError("Invalid URL format");
159
+ return false;
160
+ }
161
+ };
162
+
163
+ const updateField = <K extends keyof DirectCallConfig>(
164
+ field: K,
165
+ value: DirectCallConfig[K]
166
+ ) => {
167
+ setFormData((prev) => ({ ...prev, [field]: value }));
168
+ setError(null);
169
+ setSuccess(null);
170
+
171
+ if (field === "baseUrl") {
172
+ validateUrl(value as string);
173
+ }
174
+
175
+ // Update auth prefix based on auth type
176
+ if (field === "authType") {
177
+ const type = value as DirectCallConfig["authType"];
178
+ if (type === "bearer") {
179
+ setFormData((prev) => ({ ...prev, authHeader: "Authorization", authPrefix: "Bearer" }));
180
+ } else if (type === "basic") {
181
+ setFormData((prev) => ({ ...prev, authHeader: "Authorization", authPrefix: "Basic" }));
182
+ } else if (type === "api_key") {
183
+ setFormData((prev) => ({ ...prev, authHeader: "X-API-Key", authPrefix: "" }));
184
+ }
185
+ }
186
+ };
187
+
188
+ const handleSave = async () => {
189
+ // Validate URL
190
+ if (!validateUrl(formData.baseUrl)) {
191
+ return;
192
+ }
193
+
194
+ // Validate required fields
195
+ if (formData.authType !== "none" && !formData.masterApiKey && !formData._id) {
196
+ setError("Master API key is required for authenticated APIs");
197
+ return;
198
+ }
199
+
200
+ setIsSaving(true);
201
+ setError(null);
202
+ setSuccess(null);
203
+
204
+ try {
205
+ const token = localStorage.getItem("apiclaw_session");
206
+ if (!token) {
207
+ router.push("/providers/dashboard/login");
208
+ return;
209
+ }
210
+
211
+ await convexMutation("directCall:saveConfig", {
212
+ token,
213
+ config: {
214
+ apiId,
215
+ baseUrl: formData.baseUrl,
216
+ authType: formData.authType,
217
+ authHeader: formData.authHeader,
218
+ authPrefix: formData.authPrefix,
219
+ masterApiKey: formData.masterApiKey || undefined, // Only send if changed
220
+ rateLimitPerUser: formData.rateLimitPerUser,
221
+ rateLimitPerDay: formData.rateLimitPerDay,
222
+ pricePerRequest: formData.pricePerRequest,
223
+ status: formData.status,
224
+ allowCustomerKeys: formData.allowCustomerKeys,
225
+ requireCustomerKeys: formData.requireCustomerKeys,
226
+ },
227
+ });
228
+
229
+ setSuccess("Configuration saved successfully!");
230
+
231
+ // Clear the API key field after save (it's been encrypted)
232
+ setFormData((prev) => ({ ...prev, masterApiKey: "" }));
233
+
234
+ // Show share modal when going live for the first time
235
+ if (formData.status === "live" && !wasLiveBefore) {
236
+ setShowShareModal(true);
237
+ setWasLiveBefore(true);
238
+ }
239
+ } catch (err) {
240
+ console.error("Save failed:", err);
241
+ setError(err instanceof Error ? err.message : "Failed to save configuration");
242
+ } finally {
243
+ setIsSaving(false);
244
+ }
245
+ };
246
+
247
+ const handleStatusChange = async (newStatus: "draft" | "testing" | "live") => {
248
+ // Validate before going live
249
+ if (newStatus === "live") {
250
+ if (!formData.baseUrl || urlError) {
251
+ setError("Valid base URL required before going live");
252
+ return;
253
+ }
254
+ if (formData.authType !== "none" && !formData._id && !formData.masterApiKey) {
255
+ setError("API key must be saved before going live");
256
+ return;
257
+ }
258
+ }
259
+
260
+ updateField("status", newStatus);
261
+ };
262
+
263
+ if (isLoading) {
264
+ return (
265
+ <div className="flex items-center justify-center py-24">
266
+ <Loader2 className="w-8 h-8 text-accent animate-spin" />
267
+ </div>
268
+ );
269
+ }
270
+
271
+ if (error && !api) {
272
+ return (
273
+ <div className="text-center py-24">
274
+ <AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
275
+ <h1 className="text-2xl font-bold mb-2">Error</h1>
276
+ <p className="text-text-muted mb-6">{error}</p>
277
+ <Link href="/providers/dashboard" className="btn-primary">
278
+ Back to Dashboard
279
+ </Link>
280
+ </div>
281
+ );
282
+ }
283
+
284
+ const getStatusBadge = (status: string, isActive: boolean) => {
285
+ const baseClasses = "flex items-center gap-2 px-4 py-3 rounded-xl border cursor-pointer transition-all";
286
+
287
+ switch (status) {
288
+ case "live":
289
+ return `${baseClasses} ${
290
+ isActive
291
+ ? "border-green-500 bg-green-500/20 text-green-500"
292
+ : "border-border hover:border-green-500/50"
293
+ }`;
294
+ case "testing":
295
+ return `${baseClasses} ${
296
+ isActive
297
+ ? "border-yellow-500 bg-yellow-500/20 text-yellow-600"
298
+ : "border-border hover:border-yellow-500/50"
299
+ }`;
300
+ default:
301
+ return `${baseClasses} ${
302
+ isActive
303
+ ? "border-gray-500 bg-gray-500/20 text-gray-400"
304
+ : "border-border hover:border-gray-500/50"
305
+ }`;
306
+ }
307
+ };
308
+
309
+ return (
310
+ <div className="max-w-3xl space-y-8">
311
+ {/* Header */}
312
+ <div className="flex items-center gap-4">
313
+ <Link
314
+ href={`/providers/dashboard/${apiId}`}
315
+ className="p-2 rounded-lg hover:bg-surface transition"
316
+ >
317
+ <ChevronLeft className="w-5 h-5" />
318
+ </Link>
319
+ <div>
320
+ <h1 className="text-2xl font-bold">Direct Call Setup</h1>
321
+ <p className="text-text-muted">{api?.name}</p>
322
+ </div>
323
+ </div>
324
+
325
+ {/* Status Selector */}
326
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6">
327
+ <h2 className="font-semibold mb-4">Status</h2>
328
+ <div className="grid grid-cols-3 gap-3">
329
+ <button
330
+ onClick={() => handleStatusChange("draft")}
331
+ className={getStatusBadge("draft", formData.status === "draft")}
332
+ >
333
+ <Clock className="w-5 h-5" />
334
+ <div className="text-left">
335
+ <p className="font-medium">Draft</p>
336
+ <p className="text-xs text-text-muted">Not active</p>
337
+ </div>
338
+ </button>
339
+ <button
340
+ onClick={() => handleStatusChange("testing")}
341
+ className={getStatusBadge("testing", formData.status === "testing")}
342
+ >
343
+ <PlayCircle className="w-5 h-5" />
344
+ <div className="text-left">
345
+ <p className="font-medium">Testing</p>
346
+ <p className="text-xs text-text-muted">Test mode</p>
347
+ </div>
348
+ </button>
349
+ <button
350
+ onClick={() => handleStatusChange("live")}
351
+ className={getStatusBadge("live", formData.status === "live")}
352
+ >
353
+ <Zap className="w-5 h-5" />
354
+ <div className="text-left">
355
+ <p className="font-medium">Live</p>
356
+ <p className="text-xs text-text-muted">Production</p>
357
+ </div>
358
+ </button>
359
+ </div>
360
+ </div>
361
+
362
+ {/* Base URL */}
363
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6">
364
+ <div className="flex items-center gap-2 mb-4">
365
+ <Globe className="w-5 h-5 text-accent" />
366
+ <h2 className="font-semibold">Base URL</h2>
367
+ </div>
368
+ <div>
369
+ <input
370
+ type="url"
371
+ value={formData.baseUrl}
372
+ onChange={(e) => updateField("baseUrl", e.target.value)}
373
+ placeholder="https://api.example.com/v1"
374
+ className={`w-full px-4 py-3 rounded-xl bg-surface border focus:outline-none transition ${
375
+ urlError
376
+ ? "border-red-500 focus:border-red-500"
377
+ : "border-border focus:border-accent"
378
+ }`}
379
+ />
380
+ {urlError && (
381
+ <p className="mt-2 text-sm text-red-500 flex items-center gap-1">
382
+ <AlertCircle className="w-4 h-4" />
383
+ {urlError}
384
+ </p>
385
+ )}
386
+ <p className="mt-2 text-xs text-text-muted">
387
+ Must be HTTPS. This is the base URL for all API calls.
388
+ </p>
389
+ </div>
390
+ </div>
391
+
392
+ {/* Authentication */}
393
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6">
394
+ <div className="flex items-center gap-2 mb-4">
395
+ <Shield className="w-5 h-5 text-accent" />
396
+ <h2 className="font-semibold">Authentication</h2>
397
+ </div>
398
+
399
+ {/* Auth Type */}
400
+ <div className="space-y-4">
401
+ <div>
402
+ <label className="block text-sm font-medium mb-2">Auth Type</label>
403
+ <div className="grid grid-cols-2 gap-3">
404
+ {authTypes.map((type) => (
405
+ <button
406
+ key={type.value}
407
+ onClick={() => updateField("authType", type.value as DirectCallConfig["authType"])}
408
+ className={`text-left p-3 rounded-xl border transition ${
409
+ formData.authType === type.value
410
+ ? "border-accent bg-accent/10"
411
+ : "border-border hover:border-accent/50"
412
+ }`}
413
+ >
414
+ <p className="font-medium text-sm">{type.label}</p>
415
+ <p className="text-xs text-text-muted mt-0.5">{type.description}</p>
416
+ </button>
417
+ ))}
418
+ </div>
419
+ </div>
420
+
421
+ {formData.authType !== "none" && (
422
+ <>
423
+ {/* Auth Header */}
424
+ <div>
425
+ <label className="block text-sm font-medium mb-2">Auth Header</label>
426
+ <input
427
+ type="text"
428
+ value={formData.authHeader}
429
+ onChange={(e) => updateField("authHeader", e.target.value)}
430
+ placeholder="Authorization"
431
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
432
+ />
433
+ <p className="mt-1 text-xs text-text-muted">
434
+ The HTTP header name for authentication
435
+ </p>
436
+ </div>
437
+
438
+ {/* Master API Key */}
439
+ <div>
440
+ <label className="block text-sm font-medium mb-2">Master API Key</label>
441
+ <div className="relative">
442
+ <Key className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
443
+ <input
444
+ type={showApiKey ? "text" : "password"}
445
+ value={formData.masterApiKey}
446
+ onChange={(e) => updateField("masterApiKey", e.target.value)}
447
+ placeholder={formData._id ? "••••••••••••••••" : "Enter your API key"}
448
+ className="w-full pl-12 pr-12 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
449
+ />
450
+ <button
451
+ type="button"
452
+ onClick={() => setShowApiKey(!showApiKey)}
453
+ className="absolute right-4 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary transition"
454
+ >
455
+ {showApiKey ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
456
+ </button>
457
+ </div>
458
+ <div className="mt-2 flex items-start gap-2 text-xs text-text-muted">
459
+ <Info className="w-4 h-4 flex-shrink-0 mt-0.5" />
460
+ <span>
461
+ Your API key is encrypted with AES-256-GCM before storage.
462
+ {formData._id && " Leave blank to keep the existing key."}
463
+ </span>
464
+ </div>
465
+ </div>
466
+ </>
467
+ )}
468
+ </div>
469
+ </div>
470
+
471
+ {/* Rate Limits */}
472
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6">
473
+ <div className="flex items-center gap-2 mb-4">
474
+ <Clock className="w-5 h-5 text-accent" />
475
+ <h2 className="font-semibold">Rate Limits</h2>
476
+ </div>
477
+ <div className="grid md:grid-cols-2 gap-4">
478
+ <div>
479
+ <label className="block text-sm font-medium mb-2">Per User (requests/min)</label>
480
+ <input
481
+ type="number"
482
+ min="1"
483
+ max="10000"
484
+ value={formData.rateLimitPerUser}
485
+ onChange={(e) => updateField("rateLimitPerUser", parseInt(e.target.value) || 60)}
486
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
487
+ />
488
+ <p className="mt-1 text-xs text-text-muted">
489
+ Max requests per minute per user
490
+ </p>
491
+ </div>
492
+ <div>
493
+ <label className="block text-sm font-medium mb-2">Per Day (requests/day)</label>
494
+ <input
495
+ type="number"
496
+ min="1"
497
+ max="1000000"
498
+ value={formData.rateLimitPerDay}
499
+ onChange={(e) => updateField("rateLimitPerDay", parseInt(e.target.value) || 10000)}
500
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
501
+ />
502
+ <p className="mt-1 text-xs text-text-muted">
503
+ Max requests per day per user
504
+ </p>
505
+ </div>
506
+ </div>
507
+ </div>
508
+
509
+ {/* Customer Authentication */}
510
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6">
511
+ <div className="flex items-center gap-2 mb-4">
512
+ <Key className="w-5 h-5 text-accent" />
513
+ <h2 className="font-semibold">Customer Authentication</h2>
514
+ </div>
515
+ <p className="text-sm text-text-muted mb-4">
516
+ Control how agents authenticate with your API. By default, APIClaw uses your master key.
517
+ Enable customer keys to let agents pass their own API keys (useful for multi-tenant SaaS).
518
+ </p>
519
+ <div className="space-y-4">
520
+ <label className="flex items-start gap-3 cursor-pointer">
521
+ <input
522
+ type="checkbox"
523
+ checked={formData.allowCustomerKeys}
524
+ onChange={(e) => updateField("allowCustomerKeys", e.target.checked)}
525
+ className="mt-1 w-5 h-5 rounded border-border text-accent focus:ring-accent"
526
+ />
527
+ <div>
528
+ <span className="font-medium">Allow customer keys</span>
529
+ <p className="text-sm text-text-muted">
530
+ Agents can optionally pass their own API key via <code className="text-xs bg-surface px-1 rounded">customer_key</code> parameter
531
+ </p>
532
+ </div>
533
+ </label>
534
+ <label className="flex items-start gap-3 cursor-pointer">
535
+ <input
536
+ type="checkbox"
537
+ checked={formData.requireCustomerKeys}
538
+ onChange={(e) => updateField("requireCustomerKeys", e.target.checked)}
539
+ className="mt-1 w-5 h-5 rounded border-border text-accent focus:ring-accent"
540
+ />
541
+ <div>
542
+ <span className="font-medium">Require customer keys</span>
543
+ <p className="text-sm text-text-muted">
544
+ Agents <strong>must</strong> provide their own API key. Your master key will not be used as fallback.
545
+ <br />
546
+ <span className="text-accent">Recommended for multi-tenant SaaS where each customer has their own account.</span>
547
+ </p>
548
+ </div>
549
+ </label>
550
+ </div>
551
+ </div>
552
+
553
+ {/* Pricing */}
554
+ <div className="rounded-2xl border border-border bg-surface-elevated p-6">
555
+ <div className="flex items-center gap-2 mb-4">
556
+ <DollarSign className="w-5 h-5 text-accent" />
557
+ <h2 className="font-semibold">Pricing</h2>
558
+ </div>
559
+ <div>
560
+ <label className="block text-sm font-medium mb-2">Price per Request (credits)</label>
561
+ <input
562
+ type="number"
563
+ min="0"
564
+ step="0.1"
565
+ value={formData.pricePerRequest}
566
+ onChange={(e) => updateField("pricePerRequest", parseFloat(e.target.value) || 0)}
567
+ className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
568
+ />
569
+ <p className="mt-1 text-xs text-text-muted">
570
+ How many credits to charge per API call. Users buy credits from APIClaw.
571
+ </p>
572
+ </div>
573
+ </div>
574
+
575
+ {/* Error/Success Messages */}
576
+ {error && (
577
+ <div className="p-4 rounded-xl bg-red-500/10 border border-red-500/30 flex items-center gap-3 text-red-500">
578
+ <AlertCircle className="w-5 h-5 flex-shrink-0" />
579
+ <span className="text-sm">{error}</span>
580
+ </div>
581
+ )}
582
+
583
+ {success && (
584
+ <div className="p-4 rounded-xl bg-green-500/10 border border-green-500/30 flex items-center gap-3 text-green-500">
585
+ <Check className="w-5 h-5 flex-shrink-0" />
586
+ <span className="text-sm">{success}</span>
587
+ </div>
588
+ )}
589
+
590
+ {/* Save Button */}
591
+ <div className="flex items-center justify-between pt-4 border-t border-border">
592
+ <Link
593
+ href={`/providers/dashboard/${apiId}`}
594
+ className="btn-secondary !py-2.5"
595
+ >
596
+ Cancel
597
+ </Link>
598
+ <button
599
+ onClick={handleSave}
600
+ disabled={isSaving || !!urlError}
601
+ className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
602
+ >
603
+ {isSaving ? (
604
+ <>
605
+ <Loader2 className="w-5 h-5 animate-spin" />
606
+ Saving...
607
+ </>
608
+ ) : (
609
+ <>
610
+ <Save className="w-5 h-5" />
611
+ Save Configuration
612
+ </>
613
+ )}
614
+ </button>
615
+ </div>
616
+
617
+ {/* Next Steps */}
618
+ {formData._id && formData.status !== "live" && (
619
+ <div className="rounded-2xl border border-accent/30 bg-accent/5 p-6">
620
+ <h3 className="font-semibold mb-3">🚀 Next Steps</h3>
621
+ <ol className="space-y-3">
622
+ <li className="flex items-start gap-3 text-sm text-text-secondary">
623
+ <span className="w-6 h-6 rounded-full bg-accent/20 text-accent flex items-center justify-center flex-shrink-0 text-xs font-bold">1</span>
624
+ <span>
625
+ <Link href={`/providers/dashboard/${apiId}/actions`} className="text-accent hover:underline">
626
+ Define your Actions
627
+ </Link>
628
+ {" "}— Add the endpoints agents can call
629
+ </span>
630
+ </li>
631
+ <li className="flex items-start gap-3 text-sm text-text-secondary">
632
+ <span className="w-6 h-6 rounded-full bg-accent/20 text-accent flex items-center justify-center flex-shrink-0 text-xs font-bold">2</span>
633
+ <span>
634
+ <Link href={`/providers/dashboard/${apiId}/test`} className="text-accent hover:underline">
635
+ Test your Actions
636
+ </Link>
637
+ {" "}— Verify everything works before going live
638
+ </span>
639
+ </li>
640
+ <li className="flex items-start gap-3 text-sm text-text-secondary">
641
+ <span className="w-6 h-6 rounded-full bg-accent/20 text-accent flex items-center justify-center flex-shrink-0 text-xs font-bold">3</span>
642
+ <span>Set status to <strong>Live</strong> — Your API becomes available to all agents</span>
643
+ </li>
644
+ </ol>
645
+ </div>
646
+ )}
647
+
648
+ {/* Share Integration Modal */}
649
+ <ShareIntegrationModal
650
+ isOpen={showShareModal}
651
+ onClose={() => setShowShareModal(false)}
652
+ providerName={api?.name || ""}
653
+ apiName={api?.name || ""}
654
+ apiSlug={api?.name?.toLowerCase().replace(/\s+/g, "-") || apiId}
655
+ description={api?.description}
656
+ />
657
+ </div>
658
+ );
659
+ }