@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.
- package/AGENTS.md +74 -0
- package/HEARTBEAT.md +4 -0
- package/IDENTITY.md +22 -0
- package/README.md +197 -202
- package/SOUL.md +36 -0
- package/STATUS.md +237 -0
- package/TOOLS.md +36 -0
- package/USER.md +17 -0
- package/{backend/convex → convex}/_generated/api.d.ts +6 -6
- package/convex/credits.ts +211 -0
- package/convex/http.ts +490 -0
- package/convex/providers.ts +516 -0
- package/convex/purchases.ts +183 -0
- package/convex/schema.ts +180 -0
- package/convex.json +3 -0
- package/dist/credentials.d.ts +19 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +158 -0
- package/dist/credentials.js.map +1 -0
- package/dist/credits.d.ts +14 -11
- package/dist/credits.d.ts.map +1 -1
- package/dist/credits.js +151 -99
- package/dist/credits.js.map +1 -1
- package/dist/discovery.d.ts +7 -16
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +33 -40
- package/dist/discovery.js.map +1 -1
- package/dist/execute.d.ts +19 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +285 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.js +106 -30
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +19 -0
- package/dist/proxy.js.map +1 -0
- package/dist/registry/apis.json +95362 -202
- package/dist/registry/apis_expanded.json +100853 -0
- package/dist/stripe.d.ts +68 -0
- package/dist/stripe.d.ts.map +1 -0
- package/dist/stripe.js +196 -0
- package/dist/stripe.js.map +1 -0
- package/dist/test.d.ts +3 -2
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +105 -75
- package/dist/test.js.map +1 -1
- package/dist/types.d.ts +0 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/webhook.d.ts +2 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +90 -0
- package/dist/webhook.js.map +1 -0
- package/landing/DESIGN.md +343 -0
- package/landing/package-lock.json +1190 -40
- package/landing/package.json +5 -2
- package/landing/public/android-chrome-192x192.png +0 -0
- package/landing/public/android-chrome-512x512.png +0 -0
- package/landing/public/apple-touch-icon.png +0 -0
- package/landing/public/demo.gif +0 -0
- package/landing/public/demo.mp4 +0 -0
- package/landing/public/favicon-16x16.png +0 -0
- package/landing/public/favicon-32x32.png +0 -0
- package/landing/public/favicon.ico +0 -0
- package/landing/public/favicon.svg +3 -0
- package/landing/public/icon.svg +47 -0
- package/landing/public/logo-mono.svg +37 -0
- package/landing/public/logo-simple.svg +45 -0
- package/landing/public/logo.svg +84 -0
- package/landing/public/og-image.png +0 -0
- package/landing/public/og-template.html +184 -0
- package/landing/public/site.webmanifest +31 -0
- package/landing/scripts/generate-assets.js +284 -0
- package/landing/scripts/generate-pngs.js +48 -0
- package/landing/scripts/generate-stats.js +42 -0
- package/landing/src/app/admin/page.tsx +348 -0
- package/landing/src/app/api/auth/magic-link/route.ts +73 -0
- package/landing/src/app/api/auth/session/route.ts +38 -0
- package/landing/src/app/api/auth/verify/route.ts +43 -0
- package/landing/src/app/api/og/route.tsx +74 -0
- package/landing/src/app/globals.css +439 -100
- package/landing/src/app/layout.tsx +37 -9
- package/landing/src/app/page.tsx +640 -552
- package/landing/src/app/providers/dashboard/login/page.tsx +176 -0
- package/landing/src/app/providers/dashboard/page.tsx +589 -0
- package/landing/src/app/providers/dashboard/verify/page.tsx +106 -0
- package/landing/src/app/providers/layout.tsx +14 -0
- package/landing/src/app/providers/page.tsx +402 -0
- package/landing/src/app/providers/register/page.tsx +670 -0
- package/landing/src/components/ProviderDashboard.tsx +794 -0
- package/landing/src/hooks/useDashboardData.ts +99 -0
- package/landing/src/lib/apis.json +116054 -0
- package/landing/src/lib/convex-client.ts +106 -0
- package/landing/src/lib/mock-data.ts +285 -0
- package/landing/src/lib/stats.json +6 -0
- package/landing/tailwind.config.ts +12 -11
- package/landing/tsconfig.tsbuildinfo +1 -0
- package/package.json +21 -20
- package/scripts/SYMBOT-FIX.md +238 -0
- package/scripts/demo-simulation.py +177 -0
- package/scripts/expand-more.py +502 -0
- package/scripts/expand-registry.py +434 -0
- package/scripts/history-sanitizer.ts +272 -0
- package/scripts/mass-scrape.py +1308 -0
- package/scripts/sync-and-deploy.sh +36 -0
- package/src/credentials.ts +177 -0
- package/src/credits.ts +190 -122
- package/src/discovery.ts +45 -58
- package/src/execute.ts +350 -0
- package/src/index.ts +113 -31
- package/src/proxy.ts +24 -0
- package/src/registry/apis.json +95362 -202
- package/src/registry/apis_expanded.json +100853 -0
- package/src/stripe.ts +243 -0
- package/src/test.ts +127 -89
- package/src/types.ts +0 -34
- package/src/webhook.ts +107 -0
- package/.github/ISSUE_TEMPLATE/add-api.yml +0 -123
- package/BRIEFING.md +0 -30
- package/backend/convex/apiKeys.ts +0 -75
- package/backend/convex/purchases.ts +0 -74
- package/backend/convex/schema.ts +0 -45
- package/backend/convex/transactions.ts +0 -57
- package/backend/convex/users.ts +0 -94
- package/backend/package-lock.json +0 -521
- package/backend/package.json +0 -15
- package/dist/registry/parse_apis.py +0 -146
- package/dist/revenuecat.d.ts +0 -61
- package/dist/revenuecat.d.ts.map +0 -1
- package/dist/revenuecat.js +0 -166
- package/dist/revenuecat.js.map +0 -1
- package/dist/webhooks/revenuecat.d.ts +0 -48
- package/dist/webhooks/revenuecat.d.ts.map +0 -1
- package/dist/webhooks/revenuecat.js +0 -119
- package/dist/webhooks/revenuecat.js.map +0 -1
- package/docs/revenuecat-setup.md +0 -89
- package/landing/src/app/api/keys/route.ts +0 -71
- package/landing/src/app/api/log/route.ts +0 -37
- package/landing/src/app/api/stats/route.ts +0 -37
- package/landing/src/app/page.tsx.bak +0 -567
- package/landing/src/components/AddKeyModal.tsx +0 -159
- package/newsletter-template.html +0 -71
- package/outreach/OUTREACH-SYSTEM.md +0 -211
- package/outreach/email-template.html +0 -179
- package/outreach/targets.md +0 -133
- package/src/registry/parse_apis.py +0 -146
- package/src/revenuecat.ts +0 -239
- package/src/webhooks/revenuecat.ts +0 -187
- /package/{backend/convex → convex}/README.md +0 -0
- /package/{backend/convex → convex}/_generated/api.js +0 -0
- /package/{backend/convex → convex}/_generated/dataModel.d.ts +0 -0
- /package/{backend/convex → convex}/_generated/server.d.ts +0 -0
- /package/{backend/convex → convex}/_generated/server.js +0 -0
- /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
|
+
});
|