@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
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+ # Sync API registry stats and deploy to Vercel
3
+
4
+ cd "$(dirname "$0")/.."
5
+
6
+ echo "📊 Updating registry count..."
7
+ python3 << 'EOF'
8
+ import json
9
+ from datetime import datetime
10
+
11
+ with open('src/registry/apis.json', 'r') as f:
12
+ registry = json.load(f)
13
+
14
+ registry['count'] = len(registry['apis'])
15
+ registry['lastUpdated'] = datetime.now().strftime('%Y-%m-%d')
16
+
17
+ with open('src/registry/apis.json', 'w') as f:
18
+ json.dump(registry, f, indent=2)
19
+
20
+ print(f"✓ Registry: {registry['count']} APIs")
21
+ EOF
22
+
23
+ echo "📦 Copying apis.json to landing..."
24
+ cp src/registry/apis.json landing/src/lib/apis.json
25
+
26
+ echo "📈 Generating stats for frontend..."
27
+ cd landing
28
+ node scripts/generate-stats.js
29
+
30
+ echo "🗑️ Clearing build cache..."
31
+ rm -rf .next
32
+
33
+ echo "🚀 Deploying to Vercel..."
34
+ npx vercel --prod --force --yes
35
+
36
+ echo "✅ Done! Check https://apiclaw.nordsym.com"
@@ -0,0 +1,177 @@
1
+ // Real credential providers for APIClaw Connected tier
2
+ // Reads from environment variables or ~/.secrets/
3
+
4
+ import { readFileSync, existsSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { homedir } from 'os';
7
+ import { APICredentials } from './types.js';
8
+
9
+ interface ProviderCredential {
10
+ type: 'basic' | 'api_key' | 'bearer';
11
+ get(): APICredentials | null;
12
+ }
13
+
14
+ // Load env file helper
15
+ function loadEnvFile(filename: string): Record<string, string> {
16
+ const paths = [
17
+ join(homedir(), '.secrets', filename),
18
+ join(process.cwd(), filename),
19
+ join(process.cwd(), '.env.local'),
20
+ ];
21
+
22
+ for (const path of paths) {
23
+ if (existsSync(path)) {
24
+ const content = readFileSync(path, 'utf-8');
25
+ const vars: Record<string, string> = {};
26
+ for (const line of content.split('\n')) {
27
+ const match = line.match(/^([^=]+)=(.*)$/);
28
+ if (match) {
29
+ vars[match[1].trim()] = match[2].trim().replace(/^["']|["']$/g, '');
30
+ }
31
+ }
32
+ return vars;
33
+ }
34
+ }
35
+ return {};
36
+ }
37
+
38
+ // Provider credential getters
39
+ const providers: Record<string, ProviderCredential> = {
40
+ '46elks': {
41
+ type: 'basic',
42
+ get(): APICredentials | null {
43
+ const env = loadEnvFile('46elks.env');
44
+ const user = env.ELKS_API_USER || process.env.ELKS_API_USER;
45
+ const pass = env.ELKS_API_PASSWORD || process.env.ELKS_API_PASSWORD;
46
+
47
+ if (!user || !pass) return null;
48
+
49
+ return {
50
+ type: 'basic',
51
+ username: user,
52
+ password: pass,
53
+ };
54
+ },
55
+ },
56
+
57
+ twilio: {
58
+ type: 'basic',
59
+ get(): APICredentials | null {
60
+ const env = loadEnvFile('twilio.env');
61
+ const sid = env.TWILIO_ACCOUNT_SID || process.env.TWILIO_ACCOUNT_SID;
62
+ const token = env.TWILIO_AUTH_TOKEN || process.env.TWILIO_AUTH_TOKEN;
63
+
64
+ if (!sid || !token) return null;
65
+
66
+ return {
67
+ type: 'basic',
68
+ username: sid,
69
+ password: token,
70
+ };
71
+ },
72
+ },
73
+
74
+ // Real credential providers
75
+ resend: {
76
+ type: 'api_key',
77
+ get(): APICredentials | null {
78
+ const env = loadEnvFile('resend.env');
79
+ const key = env.RESEND_API_KEY || process.env.RESEND_API_KEY;
80
+ if (key) {
81
+ return { type: 'api_key', api_key: key };
82
+ }
83
+ return null;
84
+ },
85
+ },
86
+
87
+ brave_search: {
88
+ type: 'api_key',
89
+ get(): APICredentials | null {
90
+ const env = loadEnvFile('brave.env');
91
+ const key = env.BRAVE_API_KEY || process.env.BRAVE_API_KEY;
92
+ if (key) {
93
+ return { type: 'api_key', api_key: key };
94
+ }
95
+ return null;
96
+ },
97
+ },
98
+
99
+ openrouter: {
100
+ type: 'bearer',
101
+ get(): APICredentials | null {
102
+ const env = loadEnvFile('openrouter.env');
103
+ const key = env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY;
104
+ if (key) {
105
+ return { type: 'bearer', api_key: key };
106
+ }
107
+ return null;
108
+ },
109
+ },
110
+
111
+ elevenlabs: {
112
+ type: 'api_key',
113
+ get(): APICredentials | null {
114
+ const env = loadEnvFile('elevenlabs.env');
115
+ const key = env.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY;
116
+ if (key) {
117
+ return { type: 'api_key', api_key: key };
118
+ }
119
+ return null;
120
+ },
121
+ },
122
+ };
123
+
124
+ /**
125
+ * Get real credentials for a provider
126
+ * Returns null if provider not supported
127
+ */
128
+ export function getCredentials(providerId: string): APICredentials | null {
129
+ const provider = providers[providerId];
130
+ if (!provider) return null;
131
+ return provider.get();
132
+ }
133
+
134
+ /**
135
+ * Check if a provider has real (non-demo) credentials available
136
+ */
137
+ export function hasRealCredentials(providerId: string): boolean {
138
+ if (providerId === '46elks') {
139
+ const env = loadEnvFile('46elks.env');
140
+ return !!(env.ELKS_API_USER || process.env.ELKS_API_USER);
141
+ }
142
+ if (providerId === 'twilio') {
143
+ const env = loadEnvFile('twilio.env');
144
+ return !!(env.TWILIO_ACCOUNT_SID || process.env.TWILIO_ACCOUNT_SID);
145
+ }
146
+ if (providerId === 'resend') {
147
+ const env = loadEnvFile('resend.env');
148
+ return !!(env.RESEND_API_KEY || process.env.RESEND_API_KEY);
149
+ }
150
+ if (providerId === 'brave_search') {
151
+ const env = loadEnvFile('brave.env');
152
+ return !!(env.BRAVE_API_KEY || process.env.BRAVE_API_KEY);
153
+ }
154
+ if (providerId === 'openrouter') {
155
+ const env = loadEnvFile('openrouter.env');
156
+ return !!(env.OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY);
157
+ }
158
+ if (providerId === 'elevenlabs') {
159
+ const env = loadEnvFile('elevenlabs.env');
160
+ return !!(env.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY);
161
+ }
162
+ return false;
163
+ }
164
+
165
+ /**
166
+ * List all supported providers
167
+ */
168
+ export function listProviders(): string[] {
169
+ return Object.keys(providers);
170
+ }
171
+
172
+ /**
173
+ * Get credential type for a provider
174
+ */
175
+ export function getCredentialType(providerId: string): string | null {
176
+ return providers[providerId]?.type || null;
177
+ }
package/src/credits.ts CHANGED
@@ -1,46 +1,73 @@
1
- // Credit system for APIvault
2
- // MVP: In-memory store. Production: Supabase
1
+ // Credit system for APIClaw
2
+ // Supports both in-memory (dev) and Convex (production) backends
3
3
 
4
- import { AgentCredits, Purchase, APICredentials, UsageRecord, TransactionFee } from './types.js';
4
+ import { AgentCredits, Purchase, APICredentials, UsageRecord } from './types.js';
5
+ import { getCredentials, hasRealCredentials } from './credentials.js';
5
6
  import { randomUUID } from 'crypto';
6
- import { getFeePercentage, hasProSubscription } from './revenuecat.js';
7
-
8
- // In-memory stores (replace with Supabase in production)
9
- const agentCredits: Map<string, AgentCredits> = new Map();
10
- const purchases: Map<string, Purchase> = new Map();
11
- const usage: Map<string, UsageRecord> = new Map();
12
-
13
- // Mock API keys for demo (in production, these would be provisioned from providers)
14
- const mockCredentials: Record<string, () => APICredentials> = {
15
- '46elks': () => ({
16
- type: 'basic',
17
- username: `u_${randomUUID().slice(0, 8)}`,
18
- password: `p_${randomUUID().slice(0, 16)}`
19
- }),
20
- 'resend': () => ({
21
- type: 'api_key',
22
- api_key: `re_${randomUUID().replace(/-/g, '')}`
23
- }),
24
- 'brave_search': () => ({
25
- type: 'api_key',
26
- api_key: `BSA${randomUUID().replace(/-/g, '').toUpperCase().slice(0, 20)}`
27
- }),
28
- 'openrouter': () => ({
29
- type: 'bearer',
30
- api_key: `sk-or-v1-${randomUUID().replace(/-/g, '')}`
31
- }),
32
- 'elevenlabs': () => ({
33
- type: 'api_key',
34
- api_key: `${randomUUID().replace(/-/g, '')}`
35
- })
7
+
8
+ // Storage backend type
9
+ type StorageBackend = 'memory' | 'convex';
10
+
11
+ // Configuration
12
+ const BACKEND: StorageBackend = process.env.APICLAW_BACKEND === 'convex' ? 'convex' : 'memory';
13
+ const CONVEX_URL = process.env.CONVEX_URL || '';
14
+ const CONVEX_DEPLOY_KEY = process.env.CONVEX_DEPLOY_KEY || '';
15
+
16
+ // In-memory stores (for local development)
17
+ const agentCreditsStore: Map<string, AgentCredits> = new Map();
18
+ const purchasesStore: Map<string, Purchase> = new Map();
19
+ const usageStore: Map<string, UsageRecord> = new Map();
20
+
21
+ // Provider that have real credentials available
22
+ const REAL_CREDENTIAL_PROVIDERS = ['46elks', 'twilio'];
23
+
24
+ // Credits per dollar by provider
25
+ const CREDITS_PER_DOLLAR: Record<string, number> = {
26
+ '46elks': 30, // ~30 SMS per dollar
27
+ 'twilio': 25, // ~25 SMS per dollar
28
+ 'resend': 1000, // ~1000 emails per dollar
29
+ 'brave_search': 200, // ~200 searches per dollar
30
+ 'openrouter': 100, // ~100k tokens per dollar (varies by model)
31
+ 'elevenlabs': 3333 // ~3333 characters per dollar
36
32
  };
37
33
 
34
+ /**
35
+ * Calculate credits based on provider pricing
36
+ */
37
+ function calculateCredits(providerId: string, amountUsd: number): number {
38
+ return Math.floor(amountUsd * (CREDITS_PER_DOLLAR[providerId] || 100));
39
+ }
40
+
41
+ /**
42
+ * Generate credentials for a provider
43
+ * Returns real credentials for supported providers, mock for others
44
+ */
45
+ function generateCredentials(providerId: string): APICredentials {
46
+ // Try to get real credentials first
47
+ const realCreds = getCredentials(providerId);
48
+ if (realCreds && hasRealCredentials(providerId)) {
49
+ console.log(`[APIClaw] Using REAL credentials for ${providerId}`);
50
+ return realCreds;
51
+ }
52
+
53
+ // Fall back to mock credentials
54
+ console.log(`[APIClaw] Using MOCK credentials for ${providerId}`);
55
+ return realCreds || {
56
+ type: 'api_key',
57
+ api_key: `mock_${providerId}_${randomUUID().slice(0, 8)}`
58
+ };
59
+ }
60
+
61
+ // =============================================================================
62
+ // In-Memory Backend (for development)
63
+ // =============================================================================
64
+
38
65
  /**
39
66
  * Get or create agent credits account
40
67
  */
41
68
  export function getAgentCredits(agentId: string): AgentCredits {
42
- if (!agentCredits.has(agentId)) {
43
- agentCredits.set(agentId, {
69
+ if (!agentCreditsStore.has(agentId)) {
70
+ agentCreditsStore.set(agentId, {
44
71
  agent_id: agentId,
45
72
  balance_usd: 0,
46
73
  currency: 'USD',
@@ -48,7 +75,7 @@ export function getAgentCredits(agentId: string): AgentCredits {
48
75
  updated_at: new Date().toISOString()
49
76
  });
50
77
  }
51
- return agentCredits.get(agentId)!;
78
+ return agentCreditsStore.get(agentId)!;
52
79
  }
53
80
 
54
81
  /**
@@ -63,7 +90,7 @@ export function addCredits(agentId: string, amountUsd: number): AgentCredits {
63
90
 
64
91
  /**
65
92
  * Purchase API access
66
- * Returns credentials if successful
93
+ * Returns real credentials for 46elks and Twilio
67
94
  */
68
95
  export function purchaseAPIAccess(
69
96
  agentId: string,
@@ -80,11 +107,12 @@ export function purchaseAPIAccess(
80
107
  };
81
108
  }
82
109
 
83
- // Check if provider exists
84
- if (!mockCredentials[providerId]) {
110
+ // Check if provider is supported
111
+ const providerCredentials = getCredentials(providerId);
112
+ if (!providerCredentials) {
85
113
  return {
86
114
  success: false,
87
- error: `Unknown provider: ${providerId}`
115
+ error: `Unknown provider: ${providerId}. Supported: ${Object.keys(CREDITS_PER_DOLLAR).join(', ')}`
88
116
  };
89
117
  }
90
118
 
@@ -92,6 +120,9 @@ export function purchaseAPIAccess(
92
120
  credits.balance_usd -= amountUsd;
93
121
  credits.updated_at = new Date().toISOString();
94
122
 
123
+ // Generate credentials (real for 46elks/twilio, mock for others)
124
+ const credentials = generateCredentials(providerId);
125
+
95
126
  // Create purchase record
96
127
  const purchaseId = `pur_${randomUUID().slice(0, 12)}`;
97
128
  const purchase: Purchase = {
@@ -101,14 +132,14 @@ export function purchaseAPIAccess(
101
132
  amount_usd: amountUsd,
102
133
  credits_purchased: calculateCredits(providerId, amountUsd),
103
134
  status: 'active',
104
- credentials: mockCredentials[providerId](),
135
+ credentials,
105
136
  created_at: new Date().toISOString()
106
137
  };
107
138
 
108
- purchases.set(purchaseId, purchase);
139
+ purchasesStore.set(purchaseId, purchase);
109
140
 
110
141
  // Initialize usage tracking
111
- usage.set(purchaseId, {
142
+ usageStore.set(purchaseId, {
112
143
  purchase_id: purchaseId,
113
144
  provider_id: providerId,
114
145
  units_used: 0,
@@ -117,44 +148,32 @@ export function purchaseAPIAccess(
117
148
  last_used_at: new Date().toISOString()
118
149
  });
119
150
 
120
- return { success: true, purchase };
121
- }
122
-
123
- /**
124
- * Calculate credits based on provider pricing
125
- */
126
- function calculateCredits(providerId: string, amountUsd: number): number {
127
- // Simplified credit calculation per provider
128
- const creditsPerDollar: Record<string, number> = {
129
- '46elks': 30, // ~30 SMS per dollar
130
- 'resend': 1000, // ~1000 emails per dollar
131
- 'brave_search': 200, // ~200 searches per dollar
132
- 'openrouter': 100, // ~100k tokens per dollar (varies by model)
133
- 'elevenlabs': 3333 // ~3333 characters per dollar
134
- };
151
+ // Flag if using real credentials
152
+ const isRealCredentials = hasRealCredentials(providerId);
153
+ console.log(`[APIClaw] Purchase complete: ${providerId} ($${amountUsd}) - Real credentials: ${isRealCredentials}`);
135
154
 
136
- return Math.floor(amountUsd * (creditsPerDollar[providerId] || 100));
155
+ return { success: true, purchase };
137
156
  }
138
157
 
139
158
  /**
140
159
  * Get all purchases for an agent
141
160
  */
142
161
  export function getAgentPurchases(agentId: string): Purchase[] {
143
- return Array.from(purchases.values()).filter(p => p.agent_id === agentId);
162
+ return Array.from(purchasesStore.values()).filter(p => p.agent_id === agentId);
144
163
  }
145
164
 
146
165
  /**
147
166
  * Get usage for a purchase
148
167
  */
149
168
  export function getUsage(purchaseId: string): UsageRecord | null {
150
- return usage.get(purchaseId) || null;
169
+ return usageStore.get(purchaseId) || null;
151
170
  }
152
171
 
153
172
  /**
154
173
  * Record usage (call this when API is used)
155
174
  */
156
175
  export function recordUsage(purchaseId: string, unitsUsed: number, costUsd: number): UsageRecord | null {
157
- const record = usage.get(purchaseId);
176
+ const record = usageStore.get(purchaseId);
158
177
  if (!record) return null;
159
178
 
160
179
  record.units_used += unitsUsed;
@@ -164,7 +183,7 @@ export function recordUsage(purchaseId: string, unitsUsed: number, costUsd: numb
164
183
 
165
184
  // Update purchase status if depleted
166
185
  if (record.units_remaining === 0) {
167
- const purchase = purchases.get(purchaseId);
186
+ const purchase = purchasesStore.get(purchaseId);
168
187
  if (purchase) purchase.status = 'exhausted';
169
188
  }
170
189
 
@@ -178,84 +197,133 @@ export function getBalanceSummary(agentId: string): {
178
197
  credits: AgentCredits;
179
198
  active_purchases: Purchase[];
180
199
  total_spent_usd: number;
200
+ real_credentials_available: string[];
181
201
  } {
182
202
  const credits = getAgentCredits(agentId);
183
203
  const agentPurchases = getAgentPurchases(agentId);
184
204
  const activePurchases = agentPurchases.filter(p => p.status === 'active');
185
205
  const totalSpent = agentPurchases.reduce((sum, p) => sum + p.amount_usd, 0);
186
206
 
207
+ // List providers with real credentials
208
+ const realCredentialProviders = REAL_CREDENTIAL_PROVIDERS.filter(p => hasRealCredentials(p));
209
+
187
210
  return {
188
211
  credits,
189
212
  active_purchases: activePurchases,
190
- total_spent_usd: totalSpent
213
+ total_spent_usd: totalSpent,
214
+ real_credentials_available: realCredentialProviders
191
215
  };
192
216
  }
193
217
 
194
218
  /**
195
- * Calculate transaction fee based on user's subscription
196
- * Pro users: 2% fee
197
- * Free users: 5% fee
219
+ * Check which providers have real credentials
198
220
  */
199
- export async function calculateTransactionFee(
200
- userId: string,
201
- amountUsd: number
202
- ): Promise<TransactionFee> {
203
- const isPro = await hasProSubscription(userId);
204
- const feePercentage = isPro ? 0.02 : 0.05;
205
- const feeAmount = Math.round(amountUsd * feePercentage * 100) / 100;
206
-
207
- return {
208
- amount_usd: amountUsd,
209
- fee_percentage: feePercentage,
210
- fee_amount_usd: feeAmount,
211
- net_amount_usd: Math.round((amountUsd - feeAmount) * 100) / 100,
212
- tier: isPro ? 'pro' : 'free',
213
- };
221
+ export function getProvidersWithRealCredentials(): string[] {
222
+ return REAL_CREDENTIAL_PROVIDERS.filter(p => hasRealCredentials(p));
214
223
  }
215
224
 
216
- /**
217
- * Purchase API access with subscription-aware fees
218
- */
219
- export async function purchaseAPIAccessWithFees(
225
+ // =============================================================================
226
+ // Convex Backend (for production) - HTTP API calls
227
+ // =============================================================================
228
+
229
+ interface ConvexResponse<T> {
230
+ data?: T;
231
+ error?: string;
232
+ }
233
+
234
+ async function convexQuery<T>(path: string, args: Record<string, unknown>): Promise<ConvexResponse<T>> {
235
+ if (!CONVEX_URL) {
236
+ return { error: 'Convex URL not configured' };
237
+ }
238
+
239
+ try {
240
+ const response = await fetch(`${CONVEX_URL}/api/query`, {
241
+ method: 'POST',
242
+ headers: {
243
+ 'Content-Type': 'application/json',
244
+ ...(CONVEX_DEPLOY_KEY ? { 'Authorization': `Convex ${CONVEX_DEPLOY_KEY}` } : {}),
245
+ },
246
+ body: JSON.stringify({ path, args }),
247
+ });
248
+
249
+ const result = await response.json() as T;
250
+ return { data: result };
251
+ } catch (error) {
252
+ return { error: error instanceof Error ? error.message : 'Convex query failed' };
253
+ }
254
+ }
255
+
256
+ async function convexMutation<T>(path: string, args: Record<string, unknown>): Promise<ConvexResponse<T>> {
257
+ if (!CONVEX_URL) {
258
+ return { error: 'Convex URL not configured' };
259
+ }
260
+
261
+ try {
262
+ const response = await fetch(`${CONVEX_URL}/api/mutation`, {
263
+ method: 'POST',
264
+ headers: {
265
+ 'Content-Type': 'application/json',
266
+ ...(CONVEX_DEPLOY_KEY ? { 'Authorization': `Convex ${CONVEX_DEPLOY_KEY}` } : {}),
267
+ },
268
+ body: JSON.stringify({ path, args }),
269
+ });
270
+
271
+ const result = await response.json() as T;
272
+ return { data: result };
273
+ } catch (error) {
274
+ return { error: error instanceof Error ? error.message : 'Convex mutation failed' };
275
+ }
276
+ }
277
+
278
+ // Convex-backed versions (async)
279
+ export async function getAgentCreditsAsync(agentId: string): Promise<AgentCredits | null> {
280
+ if (BACKEND === 'memory') {
281
+ return getAgentCredits(agentId);
282
+ }
283
+
284
+ const result = await convexQuery<AgentCredits>('credits:getAgentCredits', { agentId });
285
+ return result.data || null;
286
+ }
287
+
288
+ export async function addCreditsAsync(agentId: string, amountUsd: number): Promise<AgentCredits | null> {
289
+ if (BACKEND === 'memory') {
290
+ return addCredits(agentId, amountUsd);
291
+ }
292
+
293
+ const result = await convexMutation<AgentCredits>('credits:addCredits', { agentId, amountUsd });
294
+ return result.data || null;
295
+ }
296
+
297
+ export async function purchaseAPIAccessAsync(
220
298
  agentId: string,
221
- userId: string,
222
299
  providerId: string,
223
300
  amountUsd: number
224
- ): Promise<{
225
- success: boolean;
226
- purchase?: Purchase;
227
- fee?: TransactionFee;
228
- error?: string
229
- }> {
230
- // Calculate fees first
231
- const fee = await calculateTransactionFee(userId, amountUsd);
232
-
233
- // Total amount user pays = API cost + platform fee
234
- const totalCost = amountUsd + fee.fee_amount_usd;
235
-
236
- const credits = getAgentCredits(agentId);
237
-
238
- if (credits.balance_usd < totalCost) {
239
- return {
240
- success: false,
241
- error: `Insufficient balance. Have $${credits.balance_usd.toFixed(2)}, need $${totalCost.toFixed(2)} (includes ${fee.tier === 'pro' ? '2%' : '5%'} platform fee)`
242
- };
301
+ ): Promise<{ success: boolean; purchase?: Purchase; error?: string }> {
302
+ if (BACKEND === 'memory') {
303
+ return purchaseAPIAccess(agentId, providerId, amountUsd);
243
304
  }
244
-
245
- // Make the purchase (deducts API cost)
246
- const result = purchaseAPIAccess(agentId, providerId, amountUsd);
247
-
248
- if (result.success && result.purchase) {
249
- // Deduct platform fee separately
250
- credits.balance_usd -= fee.fee_amount_usd;
251
- credits.updated_at = new Date().toISOString();
252
-
253
- return {
254
- success: true,
255
- purchase: result.purchase,
256
- fee,
257
- };
305
+
306
+ // Generate credentials server-side
307
+ const credentials = generateCredentials(providerId);
308
+
309
+ const result = await convexMutation<Purchase>('purchases:purchaseAccess', {
310
+ agentId,
311
+ providerId,
312
+ amountUsd,
313
+ credentials,
314
+ });
315
+
316
+ if (result.error) {
317
+ return { success: false, error: result.error };
258
318
  }
259
-
260
- return result;
319
+
320
+ return { success: true, purchase: result.data };
321
+ }
322
+
323
+ // Export backend info
324
+ export function getBackendInfo(): { type: StorageBackend; convexUrl: string | null } {
325
+ return {
326
+ type: BACKEND,
327
+ convexUrl: BACKEND === 'convex' ? CONVEX_URL : null,
328
+ };
261
329
  }