@nordsym/apiclaw 1.0.0 → 1.1.1

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 (161) hide show
  1. package/AGENTS.md +74 -0
  2. package/HEARTBEAT.md +4 -0
  3. package/IDENTITY.md +22 -0
  4. package/README.md +193 -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 +12 -6
  10. package/convex/analytics.ts +90 -0
  11. package/convex/credits.ts +211 -0
  12. package/convex/http.ts +578 -0
  13. package/convex/providers.ts +516 -0
  14. package/convex/purchases.ts +183 -0
  15. package/convex/ratelimit.ts +104 -0
  16. package/convex/schema.ts +220 -0
  17. package/convex/telemetry.ts +81 -0
  18. package/convex.json +3 -0
  19. package/dist/credentials.d.ts +19 -0
  20. package/dist/credentials.d.ts.map +1 -0
  21. package/dist/credentials.js +158 -0
  22. package/dist/credentials.js.map +1 -0
  23. package/dist/credits.d.ts +14 -11
  24. package/dist/credits.d.ts.map +1 -1
  25. package/dist/credits.js +151 -99
  26. package/dist/credits.js.map +1 -1
  27. package/dist/discovery.d.ts +7 -16
  28. package/dist/discovery.d.ts.map +1 -1
  29. package/dist/discovery.js +33 -40
  30. package/dist/discovery.js.map +1 -1
  31. package/dist/execute.d.ts +19 -0
  32. package/dist/execute.d.ts.map +1 -0
  33. package/dist/execute.js +285 -0
  34. package/dist/execute.js.map +1 -0
  35. package/dist/index.js +175 -31
  36. package/dist/index.js.map +1 -1
  37. package/dist/proxy.d.ts +6 -0
  38. package/dist/proxy.d.ts.map +1 -0
  39. package/dist/proxy.js +19 -0
  40. package/dist/proxy.js.map +1 -0
  41. package/dist/registry/apis.json +95362 -202
  42. package/dist/registry/apis_expanded.json +100853 -0
  43. package/dist/stripe.d.ts +68 -0
  44. package/dist/stripe.d.ts.map +1 -0
  45. package/dist/stripe.js +196 -0
  46. package/dist/stripe.js.map +1 -0
  47. package/dist/telemetry.d.ts +28 -0
  48. package/dist/telemetry.d.ts.map +1 -0
  49. package/dist/telemetry.js +50 -0
  50. package/dist/telemetry.js.map +1 -0
  51. package/dist/test.d.ts +3 -2
  52. package/dist/test.d.ts.map +1 -1
  53. package/dist/test.js +105 -75
  54. package/dist/test.js.map +1 -1
  55. package/dist/types.d.ts +0 -28
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/webhook.d.ts +2 -0
  58. package/dist/webhook.d.ts.map +1 -0
  59. package/dist/webhook.js +90 -0
  60. package/dist/webhook.js.map +1 -0
  61. package/landing/DESIGN.md +343 -0
  62. package/landing/package-lock.json +1196 -7
  63. package/landing/package.json +5 -1
  64. package/landing/public/android-chrome-192x192.png +0 -0
  65. package/landing/public/android-chrome-512x512.png +0 -0
  66. package/landing/public/apple-touch-icon.png +0 -0
  67. package/landing/public/demo.gif +0 -0
  68. package/landing/public/demo.mp4 +0 -0
  69. package/landing/public/favicon-16x16.png +0 -0
  70. package/landing/public/favicon-32x32.png +0 -0
  71. package/landing/public/favicon.ico +0 -0
  72. package/landing/public/favicon.svg +3 -0
  73. package/landing/public/icon.svg +47 -0
  74. package/landing/public/logo-mono.svg +37 -0
  75. package/landing/public/logo-simple.svg +45 -0
  76. package/landing/public/logo.svg +84 -0
  77. package/landing/public/og-template.html +184 -0
  78. package/landing/public/site.webmanifest +31 -0
  79. package/landing/scripts/generate-assets.js +284 -0
  80. package/landing/scripts/generate-pngs.js +48 -0
  81. package/landing/scripts/generate-stats.js +42 -0
  82. package/landing/src/app/admin/page.tsx +348 -0
  83. package/landing/src/app/api/auth/magic-link/route.ts +73 -0
  84. package/landing/src/app/api/auth/session/route.ts +38 -0
  85. package/landing/src/app/api/auth/verify/route.ts +43 -0
  86. package/landing/src/app/api/og/route.tsx +84 -0
  87. package/landing/src/app/globals.css +439 -100
  88. package/landing/src/app/layout.tsx +37 -7
  89. package/landing/src/app/page.tsx +627 -552
  90. package/landing/src/app/providers/dashboard/login/page.tsx +176 -0
  91. package/landing/src/app/providers/dashboard/page.tsx +589 -0
  92. package/landing/src/app/providers/dashboard/verify/page.tsx +106 -0
  93. package/landing/src/app/providers/layout.tsx +14 -0
  94. package/landing/src/app/providers/page.tsx +402 -0
  95. package/landing/src/app/providers/register/page.tsx +670 -0
  96. package/landing/src/components/ProviderDashboard.tsx +794 -0
  97. package/landing/src/hooks/useDashboardData.ts +99 -0
  98. package/landing/src/lib/apis.json +116054 -0
  99. package/landing/src/lib/convex-client.ts +106 -0
  100. package/landing/src/lib/mock-data.ts +285 -0
  101. package/landing/src/lib/stats.json +6 -0
  102. package/landing/tailwind.config.ts +12 -11
  103. package/landing/tsconfig.tsbuildinfo +1 -0
  104. package/package.json +21 -20
  105. package/scripts/SYMBOT-FIX.md +238 -0
  106. package/scripts/demo-simulation.py +177 -0
  107. package/scripts/expand-more.py +502 -0
  108. package/scripts/expand-registry.py +434 -0
  109. package/scripts/history-sanitizer.ts +272 -0
  110. package/scripts/mass-scrape.py +1308 -0
  111. package/scripts/sync-and-deploy.sh +36 -0
  112. package/src/credentials.ts +177 -0
  113. package/src/credits.ts +190 -122
  114. package/src/discovery.ts +45 -58
  115. package/src/execute.ts +350 -0
  116. package/src/index.ts +184 -32
  117. package/src/proxy.ts +24 -0
  118. package/src/registry/apis.json +95362 -202
  119. package/src/registry/apis_expanded.json +100853 -0
  120. package/src/stripe.ts +243 -0
  121. package/src/telemetry.ts +71 -0
  122. package/src/test.ts +127 -89
  123. package/src/types.ts +0 -34
  124. package/src/webhook.ts +107 -0
  125. package/.github/ISSUE_TEMPLATE/add-api.yml +0 -123
  126. package/BRIEFING.md +0 -30
  127. package/backend/convex/apiKeys.ts +0 -75
  128. package/backend/convex/purchases.ts +0 -74
  129. package/backend/convex/schema.ts +0 -45
  130. package/backend/convex/transactions.ts +0 -57
  131. package/backend/convex/users.ts +0 -94
  132. package/backend/package-lock.json +0 -521
  133. package/backend/package.json +0 -15
  134. package/dist/registry/parse_apis.py +0 -146
  135. package/dist/revenuecat.d.ts +0 -61
  136. package/dist/revenuecat.d.ts.map +0 -1
  137. package/dist/revenuecat.js +0 -166
  138. package/dist/revenuecat.js.map +0 -1
  139. package/dist/webhooks/revenuecat.d.ts +0 -48
  140. package/dist/webhooks/revenuecat.d.ts.map +0 -1
  141. package/dist/webhooks/revenuecat.js +0 -119
  142. package/dist/webhooks/revenuecat.js.map +0 -1
  143. package/docs/revenuecat-setup.md +0 -89
  144. package/landing/src/app/api/keys/route.ts +0 -71
  145. package/landing/src/app/api/log/route.ts +0 -37
  146. package/landing/src/app/api/stats/route.ts +0 -37
  147. package/landing/src/app/page.tsx.bak +0 -567
  148. package/landing/src/components/AddKeyModal.tsx +0 -159
  149. package/newsletter-template.html +0 -71
  150. package/outreach/OUTREACH-SYSTEM.md +0 -211
  151. package/outreach/email-template.html +0 -179
  152. package/outreach/targets.md +0 -133
  153. package/src/registry/parse_apis.py +0 -146
  154. package/src/revenuecat.ts +0 -239
  155. package/src/webhooks/revenuecat.ts +0 -187
  156. /package/{backend/convex → convex}/README.md +0 -0
  157. /package/{backend/convex → convex}/_generated/api.js +0 -0
  158. /package/{backend/convex → convex}/_generated/dataModel.d.ts +0 -0
  159. /package/{backend/convex → convex}/_generated/server.d.ts +0 -0
  160. /package/{backend/convex → convex}/_generated/server.js +0 -0
  161. /package/{backend/convex → convex}/tsconfig.json +0 -0
package/STATUS.md ADDED
@@ -0,0 +1,237 @@
1
+ # APIClaw Status — Verifierad 21 Feb 2026, 17:15 CET
2
+
3
+ > **Denna fil är source of truth.** Uppdatera vid varje deploy/ändring.
4
+
5
+ ---
6
+
7
+ ## 📊 Snabbstatus
8
+
9
+ | Komponent | Status | Detaljer |
10
+ |-----------|--------|----------|
11
+ | **npm** | ✅ Live | `@nordsym/apiclaw@1.0.0` (publicerad 16 feb) |
12
+ | **Landing** | ✅ Live | https://apiclaw.nordsym.com |
13
+ | **Convex** | ✅ Deployat | `brilliant-puffin-712` |
14
+ | **API Registry** | ✅ 10,001 | Version 3.0.0, uppdaterad 21 feb |
15
+ | **Kategorier** | ✅ 572 | Auto-extraherade |
16
+
17
+ ---
18
+
19
+ ## 📦 npm Package
20
+
21
+ ```
22
+ Package: @nordsym/apiclaw
23
+ Published: v1.0.0 (16 feb 2026)
24
+ Local: v0.3.0 (package.json)
25
+ Registry: https://registry.npmjs.org/@nordsym/apiclaw
26
+ ```
27
+
28
+ **⚠️ Notera:** Lokal version (0.3.0) != publicerad (1.0.0). Vid nästa publish, bumpa till 1.1.0.
29
+
30
+ **Installation:**
31
+ ```bash
32
+ npx @nordsym/apiclaw # Kör direkt
33
+ npm install @nordsym/apiclaw # Installera
34
+ ```
35
+
36
+ ---
37
+
38
+ ## 🌐 Landing Page
39
+
40
+ | | |
41
+ |-|-|
42
+ | **URL** | https://apiclaw.nordsym.com |
43
+ | **Hosting** | Vercel |
44
+ | **Project** | `landing` (prj_PmUzn4YRoL3IIBcPai2TLuIFuScE) |
45
+ | **Status** | ✅ HTTP 200 |
46
+
47
+ **Deploy:**
48
+ ```bash
49
+ cd ~/Projects/apiclaw/landing
50
+ npx vercel --prod
51
+ ```
52
+
53
+ ---
54
+
55
+ ## 🗄️ Convex Backend
56
+
57
+ | | |
58
+ |-|-|
59
+ | **Deployment** | `brilliant-puffin-712` |
60
+ | **Dashboard** | https://dashboard.convex.dev/d/brilliant-puffin-712 |
61
+ | **Status** | ✅ Deployat |
62
+
63
+ **Tabeller:**
64
+ - `providers` — API-providers (0 docs)
65
+ - `providerAPIs` — Provider-listade API:er
66
+ - `apis` — Fullt managerade API:er
67
+ - `agentCredits` — Agent credit-balanser (0 docs)
68
+ - `purchases` — Köphistorik
69
+ - `apiCalls` — Användningsloggar
70
+ - `creditTopups` — Credit-påfyllningar
71
+ - `sessions` — Provider-sessioner
72
+ - `magicLinks` — Email-auth tokens
73
+ - `payouts` — Provider-utbetalningar
74
+
75
+ **⚠️ Env vars:** Inga satta på prod. Behövs för server-side Instant Connect.
76
+
77
+ **Deploy:**
78
+ ```bash
79
+ cd ~/Projects/apiclaw
80
+ npx convex deploy
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 🔍 API Registry
86
+
87
+ | | |
88
+ |-|-|
89
+ | **Fil** | `src/registry/apis.json` |
90
+ | **Antal API:er** | 10,001 |
91
+ | **Kategorier** | 572 |
92
+ | **Version** | 3.0.0 |
93
+ | **Senast uppdaterad** | 2026-02-21 |
94
+
95
+ **Struktur:**
96
+ ```json
97
+ {
98
+ "version": "3.0.0",
99
+ "source": "apis.guru + manual",
100
+ "lastUpdated": "2026-02-21",
101
+ "count": 7692,
102
+ "apis": [...]
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## ⚡ Instant Connect
109
+
110
+ **6 providers konfigurerade lokalt:**
111
+
112
+ | Provider | Action | Credentials | Status |
113
+ |----------|--------|-------------|--------|
114
+ | `46elks` | `send_sms` | `~/.secrets/46elks.env` | ✅ Lokal |
115
+ | `twilio` | `send_sms` | `~/.secrets/twilio.env` | ✅ Lokal |
116
+ | `brave_search` | `search` | `~/.secrets/brave.env` | ✅ Lokal |
117
+ | `resend` | `send_email` | `~/.secrets/resend.env` | ✅ Lokal |
118
+ | `openrouter` | `chat` | `~/.secrets/openrouter.env` | ✅ Lokal |
119
+ | `elevenlabs` | `text_to_speech` | `~/.secrets/elevenlabs.env` | ✅ Lokal |
120
+
121
+ **Kod:** `src/execute.ts`
122
+
123
+ **⚠️ Cloud-status:** Fungerar lokalt (läser env-filer). För cloud/hosted MCP behöver credentials lagras i Convex eller env vars.
124
+
125
+ ---
126
+
127
+ ## 🔧 MCP Tools (8 st)
128
+
129
+ | Tool | Beskrivning | Status |
130
+ |------|-------------|--------|
131
+ | `discover_apis` | Sök API:er efter capability | ✅ |
132
+ | `get_api_details` | Hämta full API-info | ✅ |
133
+ | `list_categories` | Lista alla kategorier | ✅ |
134
+ | `list_connected` | Visa Instant Connect-providers | ✅ |
135
+ | `call_api` | Kör API via Instant Connect | ✅ |
136
+ | `purchase_access` | Köp API-access | ✅ |
137
+ | `check_balance` | Kolla credits | ✅ |
138
+ | `add_credits` | Lägg till test-credits | ✅ |
139
+
140
+ ---
141
+
142
+ ## 📁 Projektstruktur
143
+
144
+ ```
145
+ apiclaw/
146
+ ├── src/ # MCP Server (TypeScript)
147
+ │ ├── index.ts # Huvudfil, MCP tool definitions
148
+ │ ├── discovery.ts # Söklogik
149
+ │ ├── execute.ts # Instant Connect handlers
150
+ │ ├── credentials.ts # Credential-hantering
151
+ │ ├── credits.ts # Credit-system
152
+ │ ├── stripe.ts # Stripe (förberett)
153
+ │ └── registry/
154
+ │ └── apis.json # 7,692 API:er
155
+
156
+ ├── landing/ # Next.js frontend
157
+ │ └── src/app/
158
+ │ ├── page.tsx # Homepage
159
+ │ ├── providers/ # Provider-portal
160
+ │ └── admin/ # Admin-panel
161
+
162
+ ├── convex/ # Backend
163
+ │ ├── schema.ts # Databasschema
164
+ │ ├── providers.ts # Provider CRUD
165
+ │ └── credits.ts # Credits
166
+
167
+ ├── dist/ # Kompilerad JS
168
+ ├── package.json # v0.3.0 (lokal)
169
+ ├── STATUS.md # DENNA FIL
170
+ └── README.md # Dokumentation
171
+ ```
172
+
173
+ ---
174
+
175
+ ## 🚀 Deploy Checklist
176
+
177
+ ### Full deploy:
178
+ ```bash
179
+ cd ~/Projects/apiclaw
180
+
181
+ # 1. Bygg
182
+ npm run build
183
+
184
+ # 2. Synka registry till landing
185
+ cp src/registry/apis.json landing/src/lib/
186
+ cd landing && node scripts/generate-stats.js && cd ..
187
+
188
+ # 3. Deploy Convex
189
+ npx convex deploy
190
+
191
+ # 4. Deploy Landing
192
+ cd landing && npx vercel --prod && cd ..
193
+
194
+ # 5. Publish npm (om ny version)
195
+ npm publish --access public
196
+ ```
197
+
198
+ ### Snabb landing-deploy:
199
+ ```bash
200
+ cd ~/Projects/apiclaw
201
+ bash scripts/sync-and-deploy.sh
202
+ ```
203
+
204
+ ---
205
+
206
+ ## ❌ Vad som INTE är klart
207
+
208
+ | Feature | Prioritet | Blocker |
209
+ |---------|-----------|---------|
210
+ | Stripe payments live | Medium | Behöver webhook setup |
211
+ | Provider dashboard med live data | Medium | Ingen data i Convex |
212
+ | Cloud Instant Connect | Medium | Credentials ej i cloud |
213
+ | Rate limiting | Låg | — |
214
+ | Usage metering | Låg | Schema finns |
215
+
216
+ ---
217
+
218
+ ## 📈 Nästa steg (prioriterat)
219
+
220
+ 1. **Öka API-antal** — Scrapa fler källor för 10k+
221
+ 2. **Testa Instant Connect E2E** — Verifiera alla 6 providers
222
+ 3. **Stripe wiring** — Koppla betalflöde
223
+ 4. **Launch prep** — PH, X thread, etc.
224
+
225
+ ---
226
+
227
+ ## 📝 Ändringslogg
228
+
229
+ | Datum | Ändring |
230
+ |-------|---------|
231
+ | 2026-02-21 | STATUS.md skapad, verifierad mot prod |
232
+ | 2026-02-16 | npm v1.0.0 publicerad |
233
+ | 2026-02-21 | Registry uppdaterat till 7,692 API:er |
234
+
235
+ ---
236
+
237
+ *Senast verifierad: 21 Feb 2026, 17:15 CET*
package/TOOLS.md ADDED
@@ -0,0 +1,36 @@
1
+ # TOOLS.md - Local Notes
2
+
3
+ Skills define *how* tools work. This file is for *your* specifics — the stuff that's unique to your setup.
4
+
5
+ ## What Goes Here
6
+
7
+ Things like:
8
+ - Camera names and locations
9
+ - SSH hosts and aliases
10
+ - Preferred voices for TTS
11
+ - Speaker/room names
12
+ - Device nicknames
13
+ - Anything environment-specific
14
+
15
+ ## Examples
16
+
17
+ ```markdown
18
+ ### Cameras
19
+ - living-room → Main area, 180° wide angle
20
+ - front-door → Entrance, motion-triggered
21
+
22
+ ### SSH
23
+ - home-server → 192.168.1.100, user: admin
24
+
25
+ ### TTS
26
+ - Preferred voice: "Nova" (warm, slightly British)
27
+ - Default speaker: Kitchen HomePod
28
+ ```
29
+
30
+ ## Why Separate?
31
+
32
+ Skills are shared. Your setup is yours. Keeping them apart means you can update skills without losing your notes, and share skills without leaking your infrastructure.
33
+
34
+ ---
35
+
36
+ Add whatever helps you do your job. This is your cheat sheet.
package/USER.md ADDED
@@ -0,0 +1,17 @@
1
+ # USER.md - About Your Human
2
+
3
+ *Learn about the person you're helping. Update this as you go.*
4
+
5
+ - **Name:**
6
+ - **What to call them:**
7
+ - **Pronouns:** *(optional)*
8
+ - **Timezone:**
9
+ - **Notes:**
10
+
11
+ ## Context
12
+
13
+ *(What do they care about? What projects are they working on? What annoys them? What makes them laugh? Build this over time.)*
14
+
15
+ ---
16
+
17
+ The more you know, the better you can help. But remember — you're learning about a person, not building a dossier. Respect the difference.
@@ -8,10 +8,13 @@
8
8
  * @module
9
9
  */
10
10
 
11
- import type * as apiKeys from "../apiKeys.js";
11
+ import type * as analytics from "../analytics.js";
12
+ import type * as credits from "../credits.js";
13
+ import type * as http from "../http.js";
14
+ import type * as providers from "../providers.js";
12
15
  import type * as purchases from "../purchases.js";
13
- import type * as transactions from "../transactions.js";
14
- import type * as users from "../users.js";
16
+ import type * as ratelimit from "../ratelimit.js";
17
+ import type * as telemetry from "../telemetry.js";
15
18
 
16
19
  import type {
17
20
  ApiFromModules,
@@ -20,10 +23,13 @@ import type {
20
23
  } from "convex/server";
21
24
 
22
25
  declare const fullApi: ApiFromModules<{
23
- apiKeys: typeof apiKeys;
26
+ analytics: typeof analytics;
27
+ credits: typeof credits;
28
+ http: typeof http;
29
+ providers: typeof providers;
24
30
  purchases: typeof purchases;
25
- transactions: typeof transactions;
26
- users: typeof users;
31
+ ratelimit: typeof ratelimit;
32
+ telemetry: typeof telemetry;
27
33
  }>;
28
34
 
29
35
  /**
@@ -0,0 +1,90 @@
1
+ import { mutation, query } from "./_generated/server";
2
+ import { v } from "convex/values";
3
+
4
+ // Log an analytics event
5
+ export const log = mutation({
6
+ args: {
7
+ event: v.string(),
8
+ provider: v.optional(v.string()),
9
+ query: v.optional(v.string()),
10
+ identifier: v.string(),
11
+ metadata: v.optional(v.any()),
12
+ },
13
+ handler: async (ctx, args) => {
14
+ return await ctx.db.insert("analytics", {
15
+ ...args,
16
+ timestamp: Date.now(),
17
+ });
18
+ },
19
+ });
20
+
21
+ // Get stats for dashboard
22
+ export const getStats = query({
23
+ args: {
24
+ hoursBack: v.optional(v.number()),
25
+ },
26
+ handler: async (ctx, args) => {
27
+ const hoursBack = args.hoursBack || 24;
28
+ const since = Date.now() - hoursBack * 3600000;
29
+
30
+ const events = await ctx.db
31
+ .query("analytics")
32
+ .withIndex("by_timestamp")
33
+ .filter((q) => q.gte(q.field("timestamp"), since))
34
+ .collect();
35
+
36
+ // Aggregate stats
37
+ const stats = {
38
+ totalEvents: events.length,
39
+ discoveries: events.filter((e) => e.event === "discovery").length,
40
+ instantCalls: events.filter((e) => e.event === "instant").length,
41
+ uniqueUsers: new Set(events.map((e) => e.identifier)).size,
42
+ byProvider: {} as Record<string, number>,
43
+ topQueries: [] as { query: string; count: number }[],
44
+ hourly: [] as { hour: string; count: number }[],
45
+ };
46
+
47
+ // By provider
48
+ for (const event of events.filter((e) => e.provider)) {
49
+ stats.byProvider[event.provider!] = (stats.byProvider[event.provider!] || 0) + 1;
50
+ }
51
+
52
+ // Top queries
53
+ const queryCounts: Record<string, number> = {};
54
+ for (const event of events.filter((e) => e.query)) {
55
+ queryCounts[event.query!] = (queryCounts[event.query!] || 0) + 1;
56
+ }
57
+ stats.topQueries = Object.entries(queryCounts)
58
+ .sort(([, a], [, b]) => b - a)
59
+ .slice(0, 10)
60
+ .map(([query, count]) => ({ query, count }));
61
+
62
+ // Hourly breakdown
63
+ const hourlyCounts: Record<string, number> = {};
64
+ for (const event of events) {
65
+ const hour = new Date(event.timestamp).toISOString().slice(0, 13);
66
+ hourlyCounts[hour] = (hourlyCounts[hour] || 0) + 1;
67
+ }
68
+ stats.hourly = Object.entries(hourlyCounts)
69
+ .sort(([a], [b]) => a.localeCompare(b))
70
+ .map(([hour, count]) => ({ hour, count }));
71
+
72
+ return stats;
73
+ },
74
+ });
75
+
76
+ // Get recent events for live feed
77
+ export const getRecent = query({
78
+ args: {
79
+ limit: v.optional(v.number()),
80
+ },
81
+ handler: async (ctx, args) => {
82
+ const limit = args.limit || 50;
83
+
84
+ return await ctx.db
85
+ .query("analytics")
86
+ .withIndex("by_timestamp")
87
+ .order("desc")
88
+ .take(limit);
89
+ },
90
+ });
@@ -0,0 +1,211 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server";
3
+
4
+ // Credit packages
5
+ export const CREDIT_PACKAGES = {
6
+ starter: { amountUsd: 10, credits: 100, bonus: 0 },
7
+ growth: { amountUsd: 50, credits: 550, bonus: 50 }, // 10% bonus
8
+ scale: { amountUsd: 100, credits: 1200, bonus: 200 }, // 20% bonus
9
+ } as const;
10
+
11
+ // Get or create agent credits account
12
+ export const getOrCreateAgent = mutation({
13
+ args: { agentId: v.string() },
14
+ handler: async (ctx, args) => {
15
+ const existing = await ctx.db
16
+ .query("agentCredits")
17
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
18
+ .first();
19
+
20
+ if (existing) return existing;
21
+
22
+ const now = Date.now();
23
+ const id = await ctx.db.insert("agentCredits", {
24
+ agentId: args.agentId,
25
+ balanceUsd: 0,
26
+ currency: "USD",
27
+ createdAt: now,
28
+ updatedAt: now,
29
+ });
30
+
31
+ return await ctx.db.get(id);
32
+ },
33
+ });
34
+
35
+ // Get agent credits
36
+ export const getAgentCredits = query({
37
+ args: { agentId: v.string() },
38
+ handler: async (ctx, args) => {
39
+ return await ctx.db
40
+ .query("agentCredits")
41
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
42
+ .first();
43
+ },
44
+ });
45
+
46
+ // Add credits to agent account (called by webhook or admin)
47
+ export const addCredits = mutation({
48
+ args: {
49
+ agentId: v.string(),
50
+ amountUsd: v.number(),
51
+ source: v.optional(v.string()),
52
+ },
53
+ handler: async (ctx, args) => {
54
+ const credits = await ctx.db
55
+ .query("agentCredits")
56
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
57
+ .first();
58
+
59
+ const now = Date.now();
60
+
61
+ if (credits) {
62
+ await ctx.db.patch(credits._id, {
63
+ balanceUsd: credits.balanceUsd + args.amountUsd,
64
+ updatedAt: now,
65
+ });
66
+ return await ctx.db.get(credits._id);
67
+ } else {
68
+ const id = await ctx.db.insert("agentCredits", {
69
+ agentId: args.agentId,
70
+ balanceUsd: args.amountUsd,
71
+ currency: "USD",
72
+ createdAt: now,
73
+ updatedAt: now,
74
+ });
75
+ return await ctx.db.get(id);
76
+ }
77
+ },
78
+ });
79
+
80
+ // Deduct credits (internal use)
81
+ export const deductCredits = mutation({
82
+ args: {
83
+ agentId: v.string(),
84
+ amountUsd: v.number(),
85
+ },
86
+ handler: async (ctx, args) => {
87
+ const credits = await ctx.db
88
+ .query("agentCredits")
89
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
90
+ .first();
91
+
92
+ if (!credits) {
93
+ throw new Error(`No credits account for agent: ${args.agentId}`);
94
+ }
95
+
96
+ if (credits.balanceUsd < args.amountUsd) {
97
+ throw new Error(
98
+ `Insufficient balance: have $${credits.balanceUsd.toFixed(2)}, need $${args.amountUsd.toFixed(2)}`
99
+ );
100
+ }
101
+
102
+ await ctx.db.patch(credits._id, {
103
+ balanceUsd: credits.balanceUsd - args.amountUsd,
104
+ updatedAt: Date.now(),
105
+ });
106
+
107
+ return await ctx.db.get(credits._id);
108
+ },
109
+ });
110
+
111
+ // Record credit top-up from Stripe
112
+ export const recordTopup = mutation({
113
+ args: {
114
+ agentId: v.string(),
115
+ stripeSessionId: v.optional(v.string()),
116
+ stripePaymentIntentId: v.optional(v.string()),
117
+ amountUsd: v.number(),
118
+ creditsGranted: v.number(),
119
+ packageType: v.string(),
120
+ status: v.string(),
121
+ },
122
+ handler: async (ctx, args) => {
123
+ const now = Date.now();
124
+ return await ctx.db.insert("creditTopups", {
125
+ agentId: args.agentId,
126
+ stripeSessionId: args.stripeSessionId,
127
+ stripePaymentIntentId: args.stripePaymentIntentId,
128
+ amountUsd: args.amountUsd,
129
+ creditsGranted: args.creditsGranted,
130
+ packageType: args.packageType,
131
+ status: args.status,
132
+ createdAt: now,
133
+ completedAt: args.status === "completed" ? now : undefined,
134
+ });
135
+ },
136
+ });
137
+
138
+ // Complete a pending top-up
139
+ export const completeTopup = mutation({
140
+ args: {
141
+ stripeSessionId: v.optional(v.string()),
142
+ stripePaymentIntentId: v.optional(v.string()),
143
+ },
144
+ handler: async (ctx, args) => {
145
+ let topup;
146
+
147
+ if (args.stripeSessionId) {
148
+ topup = await ctx.db
149
+ .query("creditTopups")
150
+ .withIndex("by_stripeSessionId", (q) =>
151
+ q.eq("stripeSessionId", args.stripeSessionId)
152
+ )
153
+ .first();
154
+ } else if (args.stripePaymentIntentId) {
155
+ topup = await ctx.db
156
+ .query("creditTopups")
157
+ .withIndex("by_stripePaymentIntentId", (q) =>
158
+ q.eq("stripePaymentIntentId", args.stripePaymentIntentId)
159
+ )
160
+ .first();
161
+ }
162
+
163
+ if (!topup) {
164
+ throw new Error("Top-up not found");
165
+ }
166
+
167
+ if (topup.status === "completed") {
168
+ return topup; // Already completed, idempotent
169
+ }
170
+
171
+ // Update top-up status
172
+ await ctx.db.patch(topup._id, {
173
+ status: "completed",
174
+ completedAt: Date.now(),
175
+ });
176
+
177
+ // Add credits to agent
178
+ const credits = await ctx.db
179
+ .query("agentCredits")
180
+ .withIndex("by_agentId", (q) => q.eq("agentId", topup.agentId))
181
+ .first();
182
+
183
+ if (credits) {
184
+ await ctx.db.patch(credits._id, {
185
+ balanceUsd: credits.balanceUsd + topup.creditsGranted,
186
+ updatedAt: Date.now(),
187
+ });
188
+ } else {
189
+ await ctx.db.insert("agentCredits", {
190
+ agentId: topup.agentId,
191
+ balanceUsd: topup.creditsGranted,
192
+ currency: "USD",
193
+ createdAt: Date.now(),
194
+ updatedAt: Date.now(),
195
+ });
196
+ }
197
+
198
+ return topup;
199
+ },
200
+ });
201
+
202
+ // Get all top-ups for an agent
203
+ export const getTopups = query({
204
+ args: { agentId: v.string() },
205
+ handler: async (ctx, args) => {
206
+ return await ctx.db
207
+ .query("creditTopups")
208
+ .withIndex("by_agentId", (q) => q.eq("agentId", args.agentId))
209
+ .collect();
210
+ },
211
+ });