@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,516 @@
1
+ import { mutation, query } from "./_generated/server";
2
+ import { v } from "convex/values";
3
+
4
+ // Register a new provider and their first API
5
+ export const registerProvider = mutation({
6
+ args: {
7
+ provider: v.object({
8
+ name: v.string(),
9
+ email: v.string(),
10
+ website: v.optional(v.string()),
11
+ }),
12
+ api: v.object({
13
+ name: v.string(),
14
+ description: v.string(),
15
+ category: v.string(),
16
+ openApiUrl: v.optional(v.string()),
17
+ docsUrl: v.optional(v.string()),
18
+ pricingModel: v.string(),
19
+ pricingNotes: v.optional(v.string()),
20
+ }),
21
+ },
22
+ handler: async (ctx, args) => {
23
+ const now = Date.now();
24
+
25
+ // Check if provider already exists by email
26
+ const existing = await ctx.db
27
+ .query("providers")
28
+ .withIndex("by_email", (q) => q.eq("email", args.provider.email))
29
+ .first();
30
+
31
+ let providerId;
32
+
33
+ if (existing) {
34
+ // Use existing provider
35
+ providerId = existing._id;
36
+ } else {
37
+ // Create new provider - auto-approve for now
38
+ providerId = await ctx.db.insert("providers", {
39
+ name: args.provider.name,
40
+ email: args.provider.email,
41
+ website: args.provider.website,
42
+ status: "approved", // Auto-approve for MVP
43
+ createdAt: now,
44
+ updatedAt: now,
45
+ approvedAt: now,
46
+ });
47
+ }
48
+
49
+ // Create the API listing - auto-approve for now
50
+ const apiId = await ctx.db.insert("providerAPIs", {
51
+ providerId,
52
+ name: args.api.name,
53
+ description: args.api.description,
54
+ category: args.api.category,
55
+ openApiUrl: args.api.openApiUrl,
56
+ docsUrl: args.api.docsUrl,
57
+ pricingModel: args.api.pricingModel,
58
+ pricingNotes: args.api.pricingNotes,
59
+ status: "approved", // Auto-approve for MVP
60
+ createdAt: now,
61
+ approvedAt: now,
62
+ discoveryCount: 0,
63
+ });
64
+
65
+ return { providerId, apiId };
66
+ },
67
+ });
68
+
69
+ // Get provider by email
70
+ export const getProviderByEmail = query({
71
+ args: { email: v.string() },
72
+ handler: async (ctx, args) => {
73
+ return await ctx.db
74
+ .query("providers")
75
+ .withIndex("by_email", (q) => q.eq("email", args.email))
76
+ .first();
77
+ },
78
+ });
79
+
80
+ // Get all APIs for a provider
81
+ export const getProviderAPIs = query({
82
+ args: { providerId: v.id("providers") },
83
+ handler: async (ctx, args) => {
84
+ return await ctx.db
85
+ .query("providerAPIs")
86
+ .withIndex("by_providerId", (q) => q.eq("providerId", args.providerId))
87
+ .collect();
88
+ },
89
+ });
90
+
91
+ // Get all approved APIs (for the registry)
92
+ export const getApprovedAPIs = query({
93
+ args: {
94
+ category: v.optional(v.string()),
95
+ limit: v.optional(v.number()),
96
+ },
97
+ handler: async (ctx, args) => {
98
+ const query = ctx.db
99
+ .query("providerAPIs")
100
+ .withIndex("by_status", (q) => q.eq("status", "approved"));
101
+
102
+ const apis = await query.collect();
103
+
104
+ // Filter by category if provided
105
+ let filtered = args.category
106
+ ? apis.filter((api) => api.category === args.category)
107
+ : apis;
108
+
109
+ // Apply limit
110
+ if (args.limit) {
111
+ filtered = filtered.slice(0, args.limit);
112
+ }
113
+
114
+ return filtered;
115
+ },
116
+ });
117
+
118
+ // Get API categories with counts
119
+ export const getCategories = query({
120
+ handler: async (ctx) => {
121
+ const apis = await ctx.db
122
+ .query("providerAPIs")
123
+ .withIndex("by_status", (q) => q.eq("status", "approved"))
124
+ .collect();
125
+
126
+ const categories: Record<string, number> = {};
127
+ for (const api of apis) {
128
+ categories[api.category] = (categories[api.category] || 0) + 1;
129
+ }
130
+
131
+ return Object.entries(categories)
132
+ .map(([name, count]) => ({ name, count }))
133
+ .sort((a, b) => b.count - a.count);
134
+ },
135
+ });
136
+
137
+ // Increment discovery count when an agent finds an API
138
+ export const trackDiscovery = mutation({
139
+ args: { apiId: v.id("providerAPIs") },
140
+ handler: async (ctx, args) => {
141
+ const api = await ctx.db.get(args.apiId);
142
+ if (!api) return;
143
+
144
+ await ctx.db.patch(args.apiId, {
145
+ discoveryCount: (api.discoveryCount || 0) + 1,
146
+ lastDiscoveredAt: Date.now(),
147
+ });
148
+ },
149
+ });
150
+
151
+ // Admin: List pending providers
152
+ export const getPendingProviders = query({
153
+ handler: async (ctx) => {
154
+ return await ctx.db
155
+ .query("providers")
156
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
157
+ .collect();
158
+ },
159
+ });
160
+
161
+ // Admin: Approve provider
162
+ export const approveProvider = mutation({
163
+ args: { providerId: v.id("providers") },
164
+ handler: async (ctx, args) => {
165
+ await ctx.db.patch(args.providerId, {
166
+ status: "approved",
167
+ approvedAt: Date.now(),
168
+ updatedAt: Date.now(),
169
+ });
170
+ },
171
+ });
172
+
173
+ // Admin: Reject provider
174
+ export const rejectProvider = mutation({
175
+ args: { providerId: v.id("providers") },
176
+ handler: async (ctx, args) => {
177
+ await ctx.db.patch(args.providerId, {
178
+ status: "rejected",
179
+ updatedAt: Date.now(),
180
+ });
181
+ },
182
+ });
183
+
184
+ // Get provider stats
185
+ export const getProviderStats = query({
186
+ handler: async (ctx) => {
187
+ const providers = await ctx.db.query("providers").collect();
188
+ const apis = await ctx.db.query("providerAPIs").collect();
189
+
190
+ return {
191
+ totalProviders: providers.length,
192
+ approvedProviders: providers.filter((p) => p.status === "approved").length,
193
+ pendingProviders: providers.filter((p) => p.status === "pending").length,
194
+ totalAPIs: apis.length,
195
+ approvedAPIs: apis.filter((a) => a.status === "approved").length,
196
+ pendingAPIs: apis.filter((a) => a.status === "pending").length,
197
+ totalDiscoveries: apis.reduce((sum, a) => sum + (a.discoveryCount || 0), 0),
198
+ };
199
+ },
200
+ });
201
+
202
+ // ============================================
203
+ // DASHBOARD AUTH & SESSION FUNCTIONS
204
+ // ============================================
205
+
206
+ // Create magic link for email auth
207
+ export const createMagicLink = mutation({
208
+ args: { email: v.string() },
209
+ handler: async (ctx, { email }) => {
210
+ const token = generateToken();
211
+ const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
212
+
213
+ await ctx.db.insert("magicLinks", {
214
+ email: email.toLowerCase(),
215
+ token,
216
+ expiresAt,
217
+ createdAt: Date.now(),
218
+ });
219
+
220
+ return { token, expiresAt };
221
+ },
222
+ });
223
+
224
+ // Verify magic link and create session
225
+ export const verifyMagicLink = mutation({
226
+ args: { token: v.string() },
227
+ handler: async (ctx, { token }) => {
228
+ const magicLink = await ctx.db
229
+ .query("magicLinks")
230
+ .withIndex("by_token", (q) => q.eq("token", token))
231
+ .first();
232
+
233
+ if (!magicLink) {
234
+ return { success: false, error: "Invalid token" };
235
+ }
236
+
237
+ if (magicLink.expiresAt < Date.now()) {
238
+ return { success: false, error: "Token expired" };
239
+ }
240
+
241
+ if (magicLink.usedAt) {
242
+ return { success: false, error: "Token already used" };
243
+ }
244
+
245
+ // Mark as used
246
+ await ctx.db.patch(magicLink._id, { usedAt: Date.now() });
247
+
248
+ // Find or create provider
249
+ let provider = await ctx.db
250
+ .query("providers")
251
+ .withIndex("by_email", (q) => q.eq("email", magicLink.email))
252
+ .first();
253
+
254
+ if (!provider) {
255
+ const providerId = await ctx.db.insert("providers", {
256
+ email: magicLink.email,
257
+ name: magicLink.email.split("@")[0],
258
+ status: "approved",
259
+ createdAt: Date.now(),
260
+ updatedAt: Date.now(),
261
+ });
262
+ provider = await ctx.db.get(providerId);
263
+ }
264
+
265
+ // Create session
266
+ const sessionToken = generateToken();
267
+ const sessionExpiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000; // 30 days
268
+
269
+ await ctx.db.insert("sessions", {
270
+ providerId: provider!._id,
271
+ token: sessionToken,
272
+ expiresAt: sessionExpiresAt,
273
+ createdAt: Date.now(),
274
+ });
275
+
276
+ return {
277
+ success: true,
278
+ sessionToken,
279
+ provider: {
280
+ id: provider!._id,
281
+ email: provider!.email,
282
+ name: provider!.name,
283
+ },
284
+ };
285
+ },
286
+ });
287
+
288
+ // Get current session
289
+ export const getSession = query({
290
+ args: { token: v.string() },
291
+ handler: async (ctx, { token }) => {
292
+ const session = await ctx.db
293
+ .query("sessions")
294
+ .withIndex("by_token", (q) => q.eq("token", token))
295
+ .first();
296
+
297
+ if (!session || session.expiresAt < Date.now()) {
298
+ return null;
299
+ }
300
+
301
+ const provider = await ctx.db.get(session.providerId);
302
+ if (!provider) return null;
303
+
304
+ return {
305
+ providerId: provider._id,
306
+ email: provider.email,
307
+ name: provider.name,
308
+ stripeOnboardingComplete: (provider as any).stripeOnboardingComplete,
309
+ };
310
+ },
311
+ });
312
+
313
+ // ============================================
314
+ // DASHBOARD ANALYTICS
315
+ // ============================================
316
+
317
+ export const getAnalytics = query({
318
+ args: {
319
+ token: v.string(),
320
+ period: v.optional(v.string()), // "week", "month", "all"
321
+ },
322
+ handler: async (ctx, { token, period = "month" }) => {
323
+ const session = await ctx.db
324
+ .query("sessions")
325
+ .withIndex("by_token", (q) => q.eq("token", token))
326
+ .first();
327
+
328
+ if (!session || session.expiresAt < Date.now()) {
329
+ return null;
330
+ }
331
+
332
+ const now = Date.now();
333
+ const periodMs = {
334
+ week: 7 * 24 * 60 * 60 * 1000,
335
+ month: 30 * 24 * 60 * 60 * 1000,
336
+ all: now,
337
+ }[period] || 30 * 24 * 60 * 60 * 1000;
338
+
339
+ const startTime = now - periodMs;
340
+
341
+ // Get all API calls for this provider
342
+ const allCalls = await ctx.db
343
+ .query("apiCalls")
344
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
345
+ .collect();
346
+
347
+ const periodCalls = allCalls.filter((c) => c.timestamp >= startTime);
348
+
349
+ // Calculate metrics
350
+ 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);
353
+
354
+ // Calls over time (daily buckets)
355
+ const callsByDay: Record<string, number> = {};
356
+ const revenueByDay: Record<string, number> = {};
357
+
358
+ periodCalls.forEach((call) => {
359
+ 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;
362
+ });
363
+
364
+ // Top agents
365
+ const agentCallCounts: Record<string, number> = {};
366
+ periodCalls.forEach((call) => {
367
+ agentCallCounts[call.agentId] = (agentCallCounts[call.agentId] || 0) + 1;
368
+ });
369
+ const topAgents = Object.entries(agentCallCounts)
370
+ .sort((a, b) => b[1] - a[1])
371
+ .slice(0, 10)
372
+ .map(([agentId, calls]) => ({ agentId, calls }));
373
+
374
+ // By region
375
+ const callsByRegion: Record<string, number> = {};
376
+ periodCalls.forEach((call) => {
377
+ const region = call.region || "Unknown";
378
+ callsByRegion[region] = (callsByRegion[region] || 0) + 1;
379
+ });
380
+
381
+ // Get provider's APIs
382
+ const apis = await ctx.db
383
+ .query("providerAPIs")
384
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
385
+ .collect();
386
+
387
+ // Calls per API
388
+ const callsByApi: Record<string, number> = {};
389
+ periodCalls.forEach((call) => {
390
+ const apiIdStr = call.apiId as string;
391
+ callsByApi[apiIdStr] = (callsByApi[apiIdStr] || 0) + 1;
392
+ });
393
+
394
+ return {
395
+ totalCalls,
396
+ uniqueAgents,
397
+ totalRevenue,
398
+ callsByDay: Object.entries(callsByDay)
399
+ .map(([date, calls]) => ({
400
+ date,
401
+ calls,
402
+ revenue: revenueByDay[date] || 0,
403
+ }))
404
+ .sort((a, b) => a.date.localeCompare(b.date)),
405
+ topAgents,
406
+ callsByRegion,
407
+ apis: apis.map((api) => ({
408
+ id: api._id,
409
+ name: api.name,
410
+ calls: callsByApi[api._id as string] || 0,
411
+ status: api.status,
412
+ })),
413
+ };
414
+ },
415
+ });
416
+
417
+ // ============================================
418
+ // DASHBOARD EARNINGS
419
+ // ============================================
420
+
421
+ export const getEarnings = query({
422
+ args: { token: v.string() },
423
+ handler: async (ctx, { token }) => {
424
+ const session = await ctx.db
425
+ .query("sessions")
426
+ .withIndex("by_token", (q) => q.eq("token", token))
427
+ .first();
428
+
429
+ if (!session || session.expiresAt < Date.now()) {
430
+ return null;
431
+ }
432
+
433
+ // Get all payouts
434
+ const payouts = await ctx.db
435
+ .query("payouts")
436
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
437
+ .collect();
438
+
439
+ // Get all API calls to calculate pending
440
+ const allCalls = await ctx.db
441
+ .query("apiCalls")
442
+ .withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
443
+ .collect();
444
+
445
+ // Find last completed payout
446
+ const completedPayouts = payouts
447
+ .filter((p) => p.status === "completed")
448
+ .sort((a, b) => b.periodEnd - a.periodEnd);
449
+
450
+ const lastPayoutEnd = completedPayouts[0]?.periodEnd || 0;
451
+
452
+ // Pending = all revenue since last payout
453
+ const pendingCalls = allCalls.filter((c) => c.timestamp > lastPayoutEnd);
454
+ const pendingAmount = pendingCalls.reduce((sum, c) => sum + c.costUsd, 0);
455
+
456
+ // Total earned all time
457
+ const totalEarned = allCalls.reduce((sum, c) => sum + c.costUsd, 0);
458
+
459
+ // Get provider for Stripe status
460
+ const provider = await ctx.db.get(session.providerId);
461
+
462
+ return {
463
+ pendingAmount,
464
+ totalEarned,
465
+ totalPaidOut: completedPayouts.reduce((sum, p) => sum + p.amountUsd, 0),
466
+ stripeConnected: !!(provider as any)?.stripeConnectId,
467
+ stripeOnboardingComplete: (provider as any)?.stripeOnboardingComplete || false,
468
+ payouts: payouts
469
+ .sort((a, b) => b.createdAt - a.createdAt)
470
+ .slice(0, 20)
471
+ .map((p) => ({
472
+ id: p._id,
473
+ amount: p.amountUsd,
474
+ status: p.status,
475
+ periodStart: p.periodStart,
476
+ periodEnd: p.periodEnd,
477
+ createdAt: p.createdAt,
478
+ completedAt: p.completedAt,
479
+ })),
480
+ };
481
+ },
482
+ });
483
+
484
+ // ============================================
485
+ // ADMIN QUERIES
486
+ // ============================================
487
+
488
+ // Get all providers (admin only)
489
+ export const getAllProviders = query({
490
+ handler: async (ctx) => {
491
+ return await ctx.db
492
+ .query("providers")
493
+ .order("desc")
494
+ .collect();
495
+ },
496
+ });
497
+
498
+ // Get all APIs (admin only)
499
+ export const getAllAPIs = query({
500
+ handler: async (ctx) => {
501
+ return await ctx.db
502
+ .query("providerAPIs")
503
+ .order("desc")
504
+ .collect();
505
+ },
506
+ });
507
+
508
+ // Helper function
509
+ function generateToken(): string {
510
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
511
+ let result = "";
512
+ for (let i = 0; i < 48; i++) {
513
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
514
+ }
515
+ return result;
516
+ }
@@ -0,0 +1,183 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server";
3
+
4
+ // Provider pricing (credits per dollar)
5
+ const CREDITS_PER_DOLLAR: Record<string, number> = {
6
+ "46elks": 30, // ~30 SMS per dollar
7
+ twilio: 25, // ~25 SMS per dollar
8
+ resend: 1000, // ~1000 emails per dollar
9
+ brave_search: 200, // ~200 searches per dollar
10
+ openrouter: 100, // ~100k tokens per dollar
11
+ elevenlabs: 3333, // ~3333 characters per dollar
12
+ };
13
+
14
+ // Calculate credits for a provider purchase
15
+ function calculateCredits(providerId: string, amountUsd: number): number {
16
+ const rate = CREDITS_PER_DOLLAR[providerId] || 100;
17
+ return Math.floor(amountUsd * rate);
18
+ }
19
+
20
+ // Purchase API access
21
+ export const purchaseAccess = mutation({
22
+ args: {
23
+ agentId: v.string(),
24
+ providerId: v.string(),
25
+ amountUsd: v.number(),
26
+ credentials: v.any(), // Credentials passed from server side
27
+ },
28
+ handler: async (ctx, args) => {
29
+ // Check balance
30
+ const credits = await ctx.db
31
+ .query("agentCredits")
32
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
33
+ .first();
34
+
35
+ if (!credits || credits.balanceUsd < args.amountUsd) {
36
+ throw new Error(
37
+ `Insufficient balance: have $${(credits?.balanceUsd || 0).toFixed(2)}, need $${args.amountUsd.toFixed(2)}`
38
+ );
39
+ }
40
+
41
+ // Deduct credits
42
+ await ctx.db.patch(credits._id, {
43
+ balanceUsd: credits.balanceUsd - args.amountUsd,
44
+ updatedAt: Date.now(),
45
+ });
46
+
47
+ // Calculate credits granted
48
+ const creditsGranted = calculateCredits(args.providerId, args.amountUsd);
49
+
50
+ // Create purchase record
51
+ const purchaseId = await ctx.db.insert("purchases", {
52
+ agentId: args.agentId,
53
+ providerId: args.providerId,
54
+ amountUsd: args.amountUsd,
55
+ creditsGranted,
56
+ status: "active",
57
+ credentials: args.credentials,
58
+ createdAt: Date.now(),
59
+ });
60
+
61
+ // Initialize usage tracking
62
+ await ctx.db.insert("usage", {
63
+ purchaseId,
64
+ providerId: args.providerId,
65
+ unitsUsed: 0,
66
+ unitsRemaining: creditsGranted,
67
+ costIncurredUsd: 0,
68
+ lastUsedAt: Date.now(),
69
+ });
70
+
71
+ return await ctx.db.get(purchaseId);
72
+ },
73
+ });
74
+
75
+ // Get all purchases for an agent
76
+ export const getAgentPurchases = query({
77
+ args: { agentId: v.string() },
78
+ handler: async (ctx, args) => {
79
+ return await ctx.db
80
+ .query("purchases")
81
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
82
+ .collect();
83
+ },
84
+ });
85
+
86
+ // Get active purchase for a provider
87
+ export const getActivePurchase = query({
88
+ args: {
89
+ agentId: v.string(),
90
+ providerId: v.string(),
91
+ },
92
+ handler: async (ctx, args) => {
93
+ const purchases = await ctx.db
94
+ .query("purchases")
95
+ .withIndex("by_agentId_providerId", (q) =>
96
+ q.eq("agentId", args.agentId).eq("providerId", args.providerId)
97
+ )
98
+ .collect();
99
+
100
+ return purchases.find((p) => p.status === "active") || null;
101
+ },
102
+ });
103
+
104
+ // Get usage for a purchase
105
+ export const getUsage = query({
106
+ args: { purchaseId: v.id("purchases") },
107
+ handler: async (ctx, args) => {
108
+ return await ctx.db
109
+ .query("usage")
110
+ .withIndex("by_purchaseId", (q) => q.eq("purchaseId", args.purchaseId))
111
+ .first();
112
+ },
113
+ });
114
+
115
+ // Record usage
116
+ export const recordUsage = mutation({
117
+ args: {
118
+ purchaseId: v.id("purchases"),
119
+ unitsUsed: v.number(),
120
+ costUsd: v.number(),
121
+ },
122
+ handler: async (ctx, args) => {
123
+ const usage = await ctx.db
124
+ .query("usage")
125
+ .withIndex("by_purchaseId", (q) => q.eq("purchaseId", args.purchaseId))
126
+ .first();
127
+
128
+ if (!usage) {
129
+ throw new Error("Usage record not found");
130
+ }
131
+
132
+ const newUnitsRemaining = Math.max(0, usage.unitsRemaining - args.unitsUsed);
133
+
134
+ await ctx.db.patch(usage._id, {
135
+ unitsUsed: usage.unitsUsed + args.unitsUsed,
136
+ unitsRemaining: newUnitsRemaining,
137
+ costIncurredUsd: usage.costIncurredUsd + args.costUsd,
138
+ lastUsedAt: Date.now(),
139
+ });
140
+
141
+ // Update purchase status if depleted
142
+ if (newUnitsRemaining === 0) {
143
+ const purchase = await ctx.db.get(args.purchaseId);
144
+ if (purchase) {
145
+ await ctx.db.patch(args.purchaseId, { status: "exhausted" });
146
+ }
147
+ }
148
+
149
+ return await ctx.db
150
+ .query("usage")
151
+ .withIndex("by_purchaseId", (q) => q.eq("purchaseId", args.purchaseId))
152
+ .first();
153
+ },
154
+ });
155
+
156
+ // Get balance summary for an agent
157
+ export const getBalanceSummary = query({
158
+ args: { agentId: v.string() },
159
+ handler: async (ctx, args) => {
160
+ const credits = await ctx.db
161
+ .query("agentCredits")
162
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
163
+ .first();
164
+
165
+ const purchases = await ctx.db
166
+ .query("purchases")
167
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
168
+ .collect();
169
+
170
+ const activePurchases = purchases.filter((p) => p.status === "active");
171
+ const totalSpent = purchases.reduce((sum, p) => sum + p.amountUsd, 0);
172
+
173
+ return {
174
+ credits: credits || {
175
+ agentId: args.agentId,
176
+ balanceUsd: 0,
177
+ currency: "USD",
178
+ },
179
+ activePurchases,
180
+ totalSpentUsd: totalSpent,
181
+ };
182
+ },
183
+ });