@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
@@ -62,7 +62,18 @@ export const registerProvider = mutation({
62
62
  discoveryCount: 0,
63
63
  });
64
64
 
65
- return { providerId, apiId };
65
+ // Create session for auto-login after registration
66
+ const sessionToken = generateToken();
67
+ const sessionExpiresAt = now + 30 * 24 * 60 * 60 * 1000; // 30 days
68
+
69
+ await ctx.db.insert("sessions", {
70
+ providerId,
71
+ token: sessionToken,
72
+ expiresAt: sessionExpiresAt,
73
+ createdAt: now,
74
+ });
75
+
76
+ return { providerId, apiId, sessionToken };
66
77
  },
67
78
  });
68
79
 
@@ -314,6 +325,238 @@ export const getSession = query({
314
325
  // DASHBOARD ANALYTICS
315
326
  // ============================================
316
327
 
328
+ // Get single API by ID
329
+ export const getApiById = query({
330
+ args: { apiId: v.string() },
331
+ handler: async (ctx, args) => {
332
+ // Try to get by document ID
333
+ try {
334
+ const api = await ctx.db.get(args.apiId as any);
335
+ if (api) {
336
+ // Check if it has Direct Call configured
337
+ const directCall = await ctx.db
338
+ .query("providerDirectCall")
339
+ .filter((q) => q.eq(q.field("apiId"), args.apiId))
340
+ .first();
341
+ return { ...api, hasDirectCall: !!directCall, directCallStatus: directCall?.status };
342
+ }
343
+ } catch {
344
+ // Not a valid ID format
345
+ }
346
+ return null;
347
+ },
348
+ });
349
+
350
+ // Get provider APIs with Direct Call status
351
+ export const getProviderAPIsWithStatus = query({
352
+ args: { providerId: v.string() },
353
+ handler: async (ctx, args) => {
354
+ const apis = await ctx.db
355
+ .query("providerAPIs")
356
+ .filter((q) => q.eq(q.field("providerId"), args.providerId as any))
357
+ .collect();
358
+
359
+ // Add Direct Call status to each API
360
+ const apisWithStatus = await Promise.all(
361
+ apis.map(async (api) => {
362
+ const directCall = await ctx.db
363
+ .query("providerDirectCall")
364
+ .filter((q) => q.eq(q.field("apiId"), api._id))
365
+ .first();
366
+ return {
367
+ ...api,
368
+ hasDirectCall: !!directCall,
369
+ directCallStatus: directCall?.status,
370
+ };
371
+ })
372
+ );
373
+
374
+ return apisWithStatus;
375
+ },
376
+ });
377
+
378
+ // DEBUG: Delete API
379
+ export const debugDeleteAPI = mutation({
380
+ args: { apiId: v.string() },
381
+ handler: async (ctx, args) => {
382
+ await ctx.db.delete(args.apiId as any);
383
+ return { deleted: true };
384
+ },
385
+ });
386
+
387
+ // Add API for logged-in provider (used by register page)
388
+ export const addAPI = mutation({
389
+ args: {
390
+ token: v.string(),
391
+ api: v.object({
392
+ name: v.string(),
393
+ description: v.string(),
394
+ category: v.string(),
395
+ openApiUrl: v.optional(v.string()),
396
+ docsUrl: v.optional(v.string()),
397
+ pricingModel: v.string(),
398
+ pricingNotes: v.optional(v.string()),
399
+ }),
400
+ },
401
+ handler: async (ctx, args) => {
402
+ // Verify session
403
+ const session = await ctx.db
404
+ .query("sessions")
405
+ .withIndex("by_token", (q) => q.eq("token", args.token))
406
+ .first();
407
+
408
+ if (!session || session.expiresAt < Date.now()) {
409
+ throw new Error("Invalid or expired session");
410
+ }
411
+
412
+ const now = Date.now();
413
+ const apiId = await ctx.db.insert("providerAPIs", {
414
+ providerId: session.providerId,
415
+ name: args.api.name,
416
+ description: args.api.description,
417
+ category: args.api.category,
418
+ openApiUrl: args.api.openApiUrl,
419
+ docsUrl: args.api.docsUrl,
420
+ pricingModel: args.api.pricingModel,
421
+ pricingNotes: args.api.pricingNotes,
422
+ status: "approved",
423
+ createdAt: now,
424
+ approvedAt: now,
425
+ discoveryCount: 0,
426
+ });
427
+
428
+ return { apiId, success: true };
429
+ },
430
+ });
431
+
432
+ // Delete API for logged-in provider
433
+ export const deleteAPI = mutation({
434
+ args: {
435
+ token: v.string(),
436
+ apiId: v.string(),
437
+ },
438
+ handler: async (ctx, args) => {
439
+ // Verify session
440
+ const session = await ctx.db
441
+ .query("sessions")
442
+ .withIndex("by_token", (q) => q.eq("token", args.token))
443
+ .first();
444
+
445
+ if (!session || session.expiresAt < Date.now()) {
446
+ throw new Error("Invalid or expired session");
447
+ }
448
+
449
+ // Get the API and verify ownership
450
+ const api = await ctx.db.get(args.apiId as any);
451
+ if (!api || (api as any).providerId !== session.providerId) {
452
+ throw new Error("API not found or unauthorized");
453
+ }
454
+
455
+ // Delete the API
456
+ await ctx.db.delete(args.apiId as any);
457
+
458
+ // Also delete any Direct Call config
459
+ const directCallConfig = await ctx.db
460
+ .query("providerDirectCall")
461
+ .filter((q) => q.eq(q.field("apiId"), args.apiId))
462
+ .first();
463
+ if (directCallConfig) {
464
+ await ctx.db.delete(directCallConfig._id);
465
+ }
466
+
467
+ return { deleted: true };
468
+ },
469
+ });
470
+
471
+ // DEBUG: Update provider name
472
+ export const debugUpdateProvider = mutation({
473
+ args: {
474
+ providerId: v.string(),
475
+ name: v.optional(v.string()),
476
+ },
477
+ handler: async (ctx, args) => {
478
+ const updates: any = {};
479
+ if (args.name) updates.name = args.name;
480
+ await ctx.db.patch(args.providerId as any, updates);
481
+ return { updated: true };
482
+ },
483
+ });
484
+
485
+ // DEBUG: Add API for provider (seeding)
486
+ export const debugAddAPI = mutation({
487
+ args: {
488
+ providerId: v.string(),
489
+ name: v.string(),
490
+ description: v.string(),
491
+ category: v.string(),
492
+ docsUrl: v.optional(v.string()),
493
+ pricingModel: v.string(),
494
+ pricingNotes: v.optional(v.string()),
495
+ },
496
+ handler: async (ctx, args) => {
497
+ const now = Date.now();
498
+ return await ctx.db.insert("providerAPIs", {
499
+ providerId: args.providerId as any,
500
+ name: args.name,
501
+ description: args.description,
502
+ category: args.category,
503
+ docsUrl: args.docsUrl,
504
+ pricingModel: args.pricingModel,
505
+ pricingNotes: args.pricingNotes,
506
+ status: "approved",
507
+ createdAt: now,
508
+ approvedAt: now,
509
+ discoveryCount: 0,
510
+ });
511
+ },
512
+ });
513
+
514
+ // DEBUG: Delete provider and all related data
515
+ export const debugDeleteProvider = mutation({
516
+ args: { providerId: v.string() },
517
+ handler: async (ctx, args) => {
518
+ const providerId = args.providerId as any;
519
+
520
+ // Delete sessions
521
+ const sessions = await ctx.db.query("sessions").filter(q => q.eq(q.field("providerId"), providerId)).collect();
522
+ for (const s of sessions) await ctx.db.delete(s._id);
523
+
524
+ // Delete APIs
525
+ const apis = await ctx.db.query("providerAPIs").filter(q => q.eq(q.field("providerId"), providerId)).collect();
526
+ for (const a of apis) await ctx.db.delete(a._id);
527
+
528
+ // Delete direct call configs
529
+ const configs = await ctx.db.query("providerDirectCall").filter(q => q.eq(q.field("providerId"), providerId)).collect();
530
+ for (const c of configs) {
531
+ // Delete actions for this config
532
+ const actions = await ctx.db.query("providerActions").filter(q => q.eq(q.field("directCallId"), c._id)).collect();
533
+ for (const act of actions) await ctx.db.delete(act._id);
534
+ await ctx.db.delete(c._id);
535
+ }
536
+
537
+ // Delete provider
538
+ await ctx.db.delete(providerId);
539
+
540
+ return { deleted: true };
541
+ },
542
+ });
543
+
544
+ // DEBUG: List all sessions
545
+ export const debugListSessions = query({
546
+ args: {},
547
+ handler: async (ctx) => {
548
+ return await ctx.db.query("sessions").collect();
549
+ },
550
+ });
551
+
552
+ // DEBUG: List all providers
553
+ export const debugListProviders = query({
554
+ args: {},
555
+ handler: async (ctx) => {
556
+ return await ctx.db.query("providers").collect();
557
+ },
558
+ });
559
+
317
560
  export const getAnalytics = query({
318
561
  args: {
319
562
  token: v.string(),
@@ -338,45 +581,56 @@ export const getAnalytics = query({
338
581
 
339
582
  const startTime = now - periodMs;
340
583
 
341
- // Get all API calls for this provider
342
- const allCalls = await ctx.db
343
- .query("apiCalls")
584
+ // Get usage logs for this provider (from Direct Call usageLog)
585
+ const usageLogs = await ctx.db
586
+ .query("usageLog")
344
587
  .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
345
588
  .collect();
346
589
 
347
- const periodCalls = allCalls.filter((c) => c.timestamp >= startTime);
590
+ const periodCalls = usageLogs.filter((c) => c.timestamp >= startTime);
348
591
 
349
592
  // Calculate metrics
350
593
  const totalCalls = periodCalls.length;
351
- const uniqueAgents = new Set(periodCalls.map((c) => c.agentId)).size;
352
- const totalRevenue = periodCalls.reduce((sum, c) => sum + c.costUsd, 0);
594
+ const uniqueAgents = new Set(periodCalls.map((c) => c.userId)).size;
595
+ const totalRevenue = periodCalls.reduce((sum, c) => sum + (c.creditsUsed / 100), 0); // cents to dollars
596
+ const successCount = periodCalls.filter((c) => c.success).length;
597
+ const successRate = totalCalls > 0 ? (successCount / totalCalls) * 100 : 100;
598
+ const avgLatency = totalCalls > 0
599
+ ? periodCalls.reduce((sum, c) => sum + c.latencyMs, 0) / totalCalls
600
+ : 0;
353
601
 
354
602
  // Calls over time (daily buckets)
355
- const callsByDay: Record<string, number> = {};
356
- const revenueByDay: Record<string, number> = {};
603
+ const callsByDay: Record<string, { calls: number; revenue: number; success: number }> = {};
357
604
 
358
605
  periodCalls.forEach((call) => {
359
606
  const day = new Date(call.timestamp).toISOString().split("T")[0];
360
- callsByDay[day] = (callsByDay[day] || 0) + 1;
361
- revenueByDay[day] = (revenueByDay[day] || 0) + call.costUsd;
607
+ if (!callsByDay[day]) {
608
+ callsByDay[day] = { calls: 0, revenue: 0, success: 0 };
609
+ }
610
+ callsByDay[day].calls += 1;
611
+ callsByDay[day].revenue += call.creditsUsed / 100;
612
+ if (call.success) callsByDay[day].success += 1;
362
613
  });
363
614
 
364
- // Top agents
615
+ // Top agents (users)
365
616
  const agentCallCounts: Record<string, number> = {};
366
617
  periodCalls.forEach((call) => {
367
- agentCallCounts[call.agentId] = (agentCallCounts[call.agentId] || 0) + 1;
618
+ agentCallCounts[call.userId] = (agentCallCounts[call.userId] || 0) + 1;
368
619
  });
369
620
  const topAgents = Object.entries(agentCallCounts)
370
621
  .sort((a, b) => b[1] - a[1])
371
622
  .slice(0, 10)
372
623
  .map(([agentId, calls]) => ({ agentId, calls }));
373
624
 
374
- // By region
375
- const callsByRegion: Record<string, number> = {};
625
+ // Top actions
626
+ const actionCallCounts: Record<string, number> = {};
376
627
  periodCalls.forEach((call) => {
377
- const region = call.region || "Unknown";
378
- callsByRegion[region] = (callsByRegion[region] || 0) + 1;
628
+ actionCallCounts[call.actionName] = (actionCallCounts[call.actionName] || 0) + 1;
379
629
  });
630
+ const topActions = Object.entries(actionCallCounts)
631
+ .sort((a, b) => b[1] - a[1])
632
+ .slice(0, 10)
633
+ .map(([actionName, calls]) => ({ actionName, calls }));
380
634
 
381
635
  // Get provider's APIs
382
636
  const apis = await ctx.db
@@ -384,32 +638,93 @@ export const getAnalytics = query({
384
638
  .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
385
639
  .collect();
386
640
 
387
- // Calls per API
388
- const callsByApi: Record<string, number> = {};
641
+ // Get Direct Call configs to map directCallId to apiId
642
+ const directCallConfigs = await ctx.db
643
+ .query("providerDirectCall")
644
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
645
+ .collect();
646
+
647
+ // Calls per API (via directCallId → apiId mapping)
648
+ const callsByDirectCallId: Record<string, number> = {};
389
649
  periodCalls.forEach((call) => {
390
- const apiIdStr = call.apiId as string;
391
- callsByApi[apiIdStr] = (callsByApi[apiIdStr] || 0) + 1;
650
+ const dcId = call.directCallId as string;
651
+ callsByDirectCallId[dcId] = (callsByDirectCallId[dcId] || 0) + 1;
652
+ });
653
+
654
+ // Map to apiId
655
+ const callsByApiId: Record<string, number> = {};
656
+ directCallConfigs.forEach((dc) => {
657
+ if (dc.apiId) {
658
+ callsByApiId[dc.apiId as string] = callsByDirectCallId[dc._id as string] || 0;
659
+ }
392
660
  });
393
661
 
662
+ // Preview data for providers with no usage yet
663
+ const isPreview = totalCalls === 0;
664
+
665
+ if (isPreview) {
666
+ // Generate preview data so providers can see what the dashboard looks like
667
+ const previewDays = [];
668
+ for (let i = 13; i >= 0; i--) {
669
+ const date = new Date(now - i * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
670
+ previewDays.push({
671
+ date,
672
+ calls: Math.floor(Math.random() * 50) + 10,
673
+ revenue: Math.random() * 5,
674
+ });
675
+ }
676
+
677
+ return {
678
+ totalCalls: 847,
679
+ uniqueAgents: 23,
680
+ totalRevenue: 42.35,
681
+ successRate: 98.2,
682
+ avgLatency: 145,
683
+ callsByDay: previewDays,
684
+ topAgents: [
685
+ { agentId: "agent_demo_1", calls: 234 },
686
+ { agentId: "agent_demo_2", calls: 189 },
687
+ { agentId: "agent_demo_3", calls: 156 },
688
+ { agentId: "agent_demo_4", calls: 98 },
689
+ { agentId: "agent_demo_5", calls: 67 },
690
+ ],
691
+ topActions: [
692
+ { actionName: "send_message", calls: 412 },
693
+ { actionName: "get_status", calls: 289 },
694
+ { actionName: "create_invoice", calls: 146 },
695
+ ],
696
+ apis: apis.map((api) => ({
697
+ id: api._id,
698
+ name: api.name,
699
+ calls: Math.floor(Math.random() * 200) + 50,
700
+ status: api.status,
701
+ })),
702
+ isPreview: true,
703
+ };
704
+ }
705
+
394
706
  return {
395
707
  totalCalls,
396
708
  uniqueAgents,
397
709
  totalRevenue,
710
+ successRate,
711
+ avgLatency,
398
712
  callsByDay: Object.entries(callsByDay)
399
- .map(([date, calls]) => ({
713
+ .map(([date, data]) => ({
400
714
  date,
401
- calls,
402
- revenue: revenueByDay[date] || 0,
715
+ calls: data.calls,
716
+ revenue: data.revenue,
403
717
  }))
404
718
  .sort((a, b) => a.date.localeCompare(b.date)),
405
719
  topAgents,
406
- callsByRegion,
720
+ topActions,
407
721
  apis: apis.map((api) => ({
408
722
  id: api._id,
409
723
  name: api.name,
410
- calls: callsByApi[api._id as string] || 0,
724
+ calls: callsByApiId[api._id as string] || 0,
411
725
  status: api.status,
412
726
  })),
727
+ isPreview: false,
413
728
  };
414
729
  },
415
730
  });
package/convex/schema.ts CHANGED
@@ -217,4 +217,91 @@ export default defineSchema({
217
217
  })
218
218
  .index("by_type", ["type"])
219
219
  .index("by_timestamp", ["timestamp"]),
220
+
221
+ // ============================================
222
+ // SELF-SERVICE DIRECT CALL TABLES
223
+ // ============================================
224
+
225
+ // Provider Direct Call configuration (master key, limits, pricing)
226
+ providerDirectCall: defineTable({
227
+ providerId: v.id("providers"),
228
+ apiId: v.optional(v.id("providerAPIs")),
229
+ baseUrl: v.string(),
230
+ authType: v.string(), // "bearer" | "basic" | "api_key" | "none"
231
+ authHeader: v.string(), // e.g. "Authorization", "X-API-Key"
232
+ authPrefix: v.string(), // e.g. "Bearer ", "Basic ", ""
233
+ encryptedMasterKey: v.string(),
234
+ rateLimitPerUser: v.number(), // requests per minute per user
235
+ rateLimitPerDay: v.number(), // requests per day per user
236
+ pricePerRequest: v.number(), // in USD cents
237
+ status: v.string(), // "draft" | "testing" | "live"
238
+ // Customer key passthrough settings
239
+ allowCustomerKeys: v.optional(v.boolean()), // Allow agents to pass their own API key (default: true)
240
+ requireCustomerKeys: v.optional(v.boolean()), // Require customer key, no master key fallback (default: false)
241
+ createdAt: v.number(),
242
+ updatedAt: v.number(),
243
+ publishedAt: v.optional(v.number()),
244
+ })
245
+ .index("by_providerId", ["providerId"])
246
+ .index("by_apiId", ["apiId"])
247
+ .index("by_status", ["status"]),
248
+
249
+ // Actions defined by providers for their Direct Call APIs
250
+ providerActions: defineTable({
251
+ directCallId: v.id("providerDirectCall"),
252
+ name: v.string(), // machine name, e.g. "send_sms"
253
+ displayName: v.string(), // human-friendly, e.g. "Send SMS"
254
+ description: v.string(),
255
+ method: v.string(), // "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
256
+ path: v.string(), // e.g. "/v1/messages" or "/users/{userId}"
257
+ params: v.array(v.object({
258
+ name: v.string(),
259
+ type: v.string(), // "string" | "number" | "boolean" | "object"
260
+ required: v.boolean(),
261
+ description: v.string(),
262
+ default: v.optional(v.any()),
263
+ in: v.string(), // "body" | "query" | "path"
264
+ })),
265
+ responseMapping: v.array(v.object({
266
+ name: v.string(),
267
+ path: v.string(), // JSON path, e.g. "data.id" or "results[0].name"
268
+ })),
269
+ enabled: v.boolean(),
270
+ createdAt: v.number(),
271
+ updatedAt: v.number(),
272
+ })
273
+ .index("by_directCallId", ["directCallId"])
274
+ .index("by_directCallId_name", ["directCallId", "name"]),
275
+
276
+ // Usage logs for Direct Call actions
277
+ usageLog: defineTable({
278
+ userId: v.string(),
279
+ providerId: v.id("providers"),
280
+ directCallId: v.id("providerDirectCall"),
281
+ actionName: v.string(),
282
+ timestamp: v.number(),
283
+ success: v.boolean(),
284
+ latencyMs: v.number(),
285
+ creditsUsed: v.number(), // in USD cents
286
+ errorMessage: v.optional(v.string()),
287
+ })
288
+ .index("by_userId", ["userId"])
289
+ .index("by_providerId", ["providerId"])
290
+ .index("by_directCallId", ["directCallId"])
291
+ .index("by_timestamp", ["timestamp"])
292
+ .index("by_userId_providerId", ["userId", "providerId"])
293
+ .index("by_userId_timestamp", ["userId", "timestamp"]),
294
+
295
+ // ============================================
296
+ // WAITLIST (for Direct Call provider leads)
297
+ // ============================================
298
+
299
+ waitlist: defineTable({
300
+ email: v.string(),
301
+ type: v.string(), // "provider" | "agent" | "general"
302
+ source: v.optional(v.string()), // "landing", "docs", etc.
303
+ createdAt: v.number(),
304
+ })
305
+ .index("by_email", ["email"])
306
+ .index("by_type", ["type"]),
220
307
  });