@nordsym/apiclaw 1.3.6 → 1.3.8

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 (214) hide show
  1. package/README.md +422 -169
  2. package/convex/_generated/api.d.ts +16 -0
  3. package/convex/agents.ts +403 -0
  4. package/convex/billing.ts +651 -216
  5. package/convex/crons.ts +17 -0
  6. package/convex/directCall.ts +80 -0
  7. package/convex/earnProgress.ts +753 -0
  8. package/convex/email.ts +135 -82
  9. package/convex/feedback.ts +265 -0
  10. package/convex/http.ts +80 -4
  11. package/convex/logs.ts +304 -0
  12. package/convex/providerKeys.ts +289 -0
  13. package/convex/providers.ts +18 -0
  14. package/convex/schema.ts +185 -1
  15. package/convex/stripeActions.ts +512 -0
  16. package/convex/webhooks.ts +494 -0
  17. package/convex/workspaces.ts +158 -3
  18. package/dist/adapters/base.d.ts +112 -0
  19. package/dist/adapters/base.d.ts.map +1 -0
  20. package/dist/adapters/base.js +247 -0
  21. package/dist/adapters/base.js.map +1 -0
  22. package/dist/adapters/claude-desktop.d.ts +12 -0
  23. package/dist/adapters/claude-desktop.d.ts.map +1 -0
  24. package/dist/adapters/claude-desktop.js +36 -0
  25. package/dist/adapters/claude-desktop.js.map +1 -0
  26. package/dist/adapters/cline.d.ts +20 -0
  27. package/dist/adapters/cline.d.ts.map +1 -0
  28. package/dist/adapters/cline.js +77 -0
  29. package/dist/adapters/cline.js.map +1 -0
  30. package/dist/adapters/continue.d.ts +26 -0
  31. package/dist/adapters/continue.d.ts.map +1 -0
  32. package/dist/adapters/continue.js +68 -0
  33. package/dist/adapters/continue.js.map +1 -0
  34. package/dist/adapters/cursor.d.ts +12 -0
  35. package/dist/adapters/cursor.d.ts.map +1 -0
  36. package/dist/adapters/cursor.js +38 -0
  37. package/dist/adapters/cursor.js.map +1 -0
  38. package/dist/adapters/custom.d.ts +47 -0
  39. package/dist/adapters/custom.d.ts.map +1 -0
  40. package/dist/adapters/custom.js +146 -0
  41. package/dist/adapters/custom.js.map +1 -0
  42. package/dist/adapters/detect.d.ts +69 -0
  43. package/dist/adapters/detect.d.ts.map +1 -0
  44. package/dist/adapters/detect.js +158 -0
  45. package/dist/adapters/detect.js.map +1 -0
  46. package/dist/adapters/index.d.ts +21 -0
  47. package/dist/adapters/index.d.ts.map +1 -0
  48. package/dist/adapters/index.js +23 -0
  49. package/dist/adapters/index.js.map +1 -0
  50. package/dist/adapters/windsurf.d.ts +12 -0
  51. package/dist/adapters/windsurf.d.ts.map +1 -0
  52. package/dist/adapters/windsurf.js +39 -0
  53. package/dist/adapters/windsurf.js.map +1 -0
  54. package/dist/bin.d.ts +9 -0
  55. package/dist/bin.d.ts.map +1 -0
  56. package/dist/bin.js +19 -0
  57. package/dist/bin.js.map +1 -0
  58. package/dist/cli/commands/doctor.d.ts +34 -0
  59. package/dist/cli/commands/doctor.d.ts.map +1 -0
  60. package/dist/cli/commands/doctor.js +312 -0
  61. package/dist/cli/commands/doctor.js.map +1 -0
  62. package/dist/cli/commands/index.d.ts +9 -0
  63. package/dist/cli/commands/index.d.ts.map +1 -0
  64. package/dist/cli/commands/index.js +9 -0
  65. package/dist/cli/commands/index.js.map +1 -0
  66. package/dist/cli/commands/restore.d.ts +50 -0
  67. package/dist/cli/commands/restore.d.ts.map +1 -0
  68. package/dist/cli/commands/restore.js +260 -0
  69. package/dist/cli/commands/restore.js.map +1 -0
  70. package/dist/cli/commands/setup.d.ts +19 -0
  71. package/dist/cli/commands/setup.d.ts.map +1 -0
  72. package/dist/cli/commands/setup.js +206 -0
  73. package/dist/cli/commands/setup.js.map +1 -0
  74. package/dist/cli/commands/uninstall.d.ts +37 -0
  75. package/dist/cli/commands/uninstall.d.ts.map +1 -0
  76. package/dist/cli/commands/uninstall.js +189 -0
  77. package/dist/cli/commands/uninstall.js.map +1 -0
  78. package/dist/cli/index.d.ts +7 -0
  79. package/dist/cli/index.d.ts.map +1 -0
  80. package/dist/cli/index.js +97 -0
  81. package/dist/cli/index.js.map +1 -0
  82. package/dist/discovery.d.ts +6 -2
  83. package/dist/discovery.d.ts.map +1 -1
  84. package/dist/discovery.js +296 -2
  85. package/dist/discovery.js.map +1 -1
  86. package/dist/enterprise/env.d.ts +56 -0
  87. package/dist/enterprise/env.d.ts.map +1 -0
  88. package/dist/enterprise/env.js +124 -0
  89. package/dist/enterprise/env.js.map +1 -0
  90. package/dist/enterprise/index.d.ts +7 -0
  91. package/dist/enterprise/index.d.ts.map +1 -0
  92. package/dist/enterprise/index.js +7 -0
  93. package/dist/enterprise/index.js.map +1 -0
  94. package/dist/enterprise/script-generator.d.ts +32 -0
  95. package/dist/enterprise/script-generator.d.ts.map +1 -0
  96. package/dist/enterprise/script-generator.js +461 -0
  97. package/dist/enterprise/script-generator.js.map +1 -0
  98. package/dist/execute.d.ts +21 -0
  99. package/dist/execute.d.ts.map +1 -1
  100. package/dist/execute.js +231 -0
  101. package/dist/execute.js.map +1 -1
  102. package/dist/index.js +257 -7
  103. package/dist/index.js.map +1 -1
  104. package/dist/metered.d.ts +62 -0
  105. package/dist/metered.d.ts.map +1 -0
  106. package/dist/metered.js +81 -0
  107. package/dist/metered.js.map +1 -0
  108. package/dist/stripe.d.ts +62 -0
  109. package/dist/stripe.d.ts.map +1 -1
  110. package/dist/stripe.js +212 -0
  111. package/dist/stripe.js.map +1 -1
  112. package/dist/types.d.ts +29 -0
  113. package/dist/types.d.ts.map +1 -1
  114. package/dist/ui/colors.d.ts +111 -0
  115. package/dist/ui/colors.d.ts.map +1 -0
  116. package/dist/ui/colors.js +185 -0
  117. package/dist/ui/colors.js.map +1 -0
  118. package/dist/ui/errors.d.ts +69 -0
  119. package/dist/ui/errors.d.ts.map +1 -0
  120. package/dist/ui/errors.js +334 -0
  121. package/dist/ui/errors.js.map +1 -0
  122. package/dist/ui/index.d.ts +10 -0
  123. package/dist/ui/index.d.ts.map +1 -0
  124. package/dist/ui/index.js +14 -0
  125. package/dist/ui/index.js.map +1 -0
  126. package/dist/ui/prompts.d.ts +88 -0
  127. package/dist/ui/prompts.d.ts.map +1 -0
  128. package/dist/ui/prompts.js +295 -0
  129. package/dist/ui/prompts.js.map +1 -0
  130. package/dist/ui/spinner.d.ts +112 -0
  131. package/dist/ui/spinner.d.ts.map +1 -0
  132. package/dist/ui/spinner.js +229 -0
  133. package/dist/ui/spinner.js.map +1 -0
  134. package/dist/utils/backup.d.ts +48 -0
  135. package/dist/utils/backup.d.ts.map +1 -0
  136. package/dist/utils/backup.js +182 -0
  137. package/dist/utils/backup.js.map +1 -0
  138. package/dist/utils/config.d.ts +80 -0
  139. package/dist/utils/config.d.ts.map +1 -0
  140. package/dist/utils/config.js +221 -0
  141. package/dist/utils/config.js.map +1 -0
  142. package/dist/utils/os.d.ts +45 -0
  143. package/dist/utils/os.d.ts.map +1 -0
  144. package/dist/utils/os.js +106 -0
  145. package/dist/utils/os.js.map +1 -0
  146. package/dist/utils/paths.d.ts +38 -0
  147. package/dist/utils/paths.d.ts.map +1 -0
  148. package/dist/utils/paths.js +160 -0
  149. package/dist/utils/paths.js.map +1 -0
  150. package/docs/PRD-BILLING.md +226 -0
  151. package/docs/PRD-EARN-SYSTEM.md +261 -0
  152. package/docs/PRD-MCP-AUTO-SETUP.md +623 -0
  153. package/docs/PRD-final-polish.md +117 -0
  154. package/docs/PRD-mobile-responsive.md +56 -0
  155. package/docs/PRD-navigation-expansion.md +295 -0
  156. package/docs/PRD-stripe-billing.md +312 -0
  157. package/docs/PRD-workspace-cleanup.md +200 -0
  158. package/docs/enterprise-deployment.md +728 -0
  159. package/landing/next.config.mjs +14 -0
  160. package/landing/public/stats.json +4 -2
  161. package/landing/scripts/generate-stats.js +12 -0
  162. package/landing/src/app/api/billing/checkout/route.ts +109 -0
  163. package/landing/src/app/api/billing/payment-method/route.ts +118 -0
  164. package/landing/src/app/api/billing/portal/route.ts +64 -0
  165. package/landing/src/app/api/workspace-auth/magic-link/route.ts +6 -3
  166. package/landing/src/app/auth/verify/page.tsx +31 -9
  167. package/landing/src/app/docs/page.tsx +1 -1
  168. package/landing/src/app/earn/page.tsx +6 -6
  169. package/landing/src/app/join/page.tsx +49 -0
  170. package/landing/src/app/login/page.tsx +8 -2
  171. package/landing/src/app/page.tsx +81 -96
  172. package/landing/src/app/providers/dashboard/page.tsx +1 -1
  173. package/landing/src/app/providers/register/page.tsx +1 -1
  174. package/landing/src/app/workspace/page.tsx +3269 -534
  175. package/landing/src/components/CheckoutButton.tsx +188 -0
  176. package/landing/src/components/EarnCreditsTab.tsx +842 -0
  177. package/landing/src/components/Toast.tsx +84 -0
  178. package/landing/src/lib/stats.json +3 -1
  179. package/package.json +9 -2
  180. package/src/adapters/base.ts +363 -0
  181. package/src/adapters/claude-desktop.ts +41 -0
  182. package/src/adapters/cline.ts +88 -0
  183. package/src/adapters/continue.ts +91 -0
  184. package/src/adapters/cursor.ts +43 -0
  185. package/src/adapters/custom.ts +188 -0
  186. package/src/adapters/detect.ts +202 -0
  187. package/src/adapters/index.ts +47 -0
  188. package/src/adapters/windsurf.ts +44 -0
  189. package/src/bin.ts +19 -0
  190. package/src/cli/commands/doctor.ts +367 -0
  191. package/src/cli/commands/index.ts +9 -0
  192. package/src/cli/commands/restore.ts +333 -0
  193. package/src/cli/commands/setup.ts +276 -0
  194. package/src/cli/commands/uninstall.ts +240 -0
  195. package/src/cli/index.ts +107 -0
  196. package/src/discovery.ts +328 -3
  197. package/src/enterprise/env.ts +156 -0
  198. package/src/enterprise/index.ts +7 -0
  199. package/src/enterprise/script-generator.ts +481 -0
  200. package/src/execute.ts +256 -0
  201. package/src/index.ts +290 -7
  202. package/src/metered.ts +149 -0
  203. package/src/stripe.ts +253 -0
  204. package/src/types.ts +32 -0
  205. package/src/ui/colors.ts +219 -0
  206. package/src/ui/errors.ts +394 -0
  207. package/src/ui/index.ts +17 -0
  208. package/src/ui/prompts.ts +390 -0
  209. package/src/ui/spinner.ts +325 -0
  210. package/src/utils/backup.ts +224 -0
  211. package/src/utils/config.ts +315 -0
  212. package/src/utils/os.ts +124 -0
  213. package/src/utils/paths.ts +203 -0
  214. package/landing/tsconfig.tsbuildinfo +0 -1
@@ -1,6 +1,20 @@
1
1
  /** @type {import('next').NextConfig} */
2
2
  const nextConfig = {
3
3
  output: 'standalone',
4
+ async redirects() {
5
+ return [
6
+ {
7
+ source: '/docs',
8
+ destination: '/workspace?tab=docs',
9
+ permanent: false,
10
+ },
11
+ {
12
+ source: '/faq',
13
+ destination: '/#faq',
14
+ permanent: false,
15
+ },
16
+ ];
17
+ },
4
18
  };
5
19
 
6
20
  export default nextConfig;
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "apiCount": 22392,
3
+ "openApiCount": 996,
4
+ "directCallCount": 10,
3
5
  "categoryCount": 14,
4
- "lastUpdated": "2026-02-27T12:05:00.000Z",
5
- "generatedAt": "2026-02-27T12:05:00.000Z",
6
+ "lastUpdated": "2026-02-28T21:45:00.000Z",
7
+ "generatedAt": "2026-02-28T21:45:00.000Z",
6
8
  "categoryBreakdown": {
7
9
  "Utilities": 7069,
8
10
  "Analytics": 2600,
@@ -88,8 +88,18 @@ try {
88
88
  });
89
89
  const uniqueCategories = Object.keys(categoryBreakdown);
90
90
 
91
+ // Count open APIs (no auth required) - case insensitive
92
+ const openApiCount = registry.apis.filter(api =>
93
+ !api.auth || api.auth === '' || api.auth.toLowerCase() === 'none'
94
+ ).length;
95
+
96
+ // Direct call providers (hardcoded for now, update when adding new providers)
97
+ const directCallCount = 10;
98
+
91
99
  const stats = {
92
100
  apiCount: registry.count,
101
+ openApiCount: openApiCount,
102
+ directCallCount: directCallCount,
93
103
  categoryCount: uniqueCategories.length,
94
104
  lastUpdated: registry.lastUpdated || new Date().toISOString().split('T')[0],
95
105
  generatedAt: new Date().toISOString(),
@@ -109,6 +119,8 @@ try {
109
119
  // Write fallback stats
110
120
  const fallback = {
111
121
  apiCount: 22392,
122
+ openApiCount: 996,
123
+ directCallCount: 10,
112
124
  categoryCount: 14,
113
125
  lastUpdated: new Date().toISOString().split('T')[0],
114
126
  generatedAt: new Date().toISOString(),
@@ -0,0 +1,109 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import Stripe from "stripe";
3
+
4
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
5
+ const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
6
+
7
+ const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null;
8
+
9
+ // Price ID for usage-based subscription
10
+ const USAGE_BASED_PRICE_ID = process.env.STRIPE_USAGE_PRICE_ID || "price_1R9hAUCQSPYXHCfhMmaPnmN9";
11
+
12
+ export async function POST(req: NextRequest) {
13
+ if (!stripe) {
14
+ return NextResponse.json({ error: "Stripe not configured" }, { status: 500 });
15
+ }
16
+
17
+ try {
18
+ const { token } = await req.json();
19
+
20
+ if (!token) {
21
+ return NextResponse.json({ error: "Missing token" }, { status: 400 });
22
+ }
23
+
24
+ // Verify session and get workspace
25
+ const sessionRes = await fetch(`${CONVEX_URL}/api/query`, {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({
29
+ path: "workspaces:getSessionWorkspace",
30
+ args: { token },
31
+ }),
32
+ });
33
+
34
+ const sessionData = await sessionRes.json();
35
+ const workspace = sessionData.value || sessionData;
36
+
37
+ if (!workspace || !workspace._id) {
38
+ return NextResponse.json({ error: "Invalid session" }, { status: 401 });
39
+ }
40
+
41
+ // Check if customer already exists
42
+ let customerId = workspace.stripeCustomerId;
43
+
44
+ if (!customerId) {
45
+ // Create new Stripe customer
46
+ const customer = await stripe.customers.create({
47
+ email: workspace.email,
48
+ metadata: {
49
+ workspaceId: workspace._id,
50
+ },
51
+ });
52
+
53
+ customerId = customer.id;
54
+
55
+ // Save customer ID to workspace
56
+ await fetch(`${CONVEX_URL}/api/mutation`, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body: JSON.stringify({
60
+ path: "billing:linkCustomer",
61
+ args: {
62
+ workspaceId: workspace._id,
63
+ stripeCustomerId: customerId,
64
+ },
65
+ }),
66
+ });
67
+ }
68
+
69
+ // Determine the app URL for redirects
70
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.com";
71
+
72
+ // Create Checkout Session for setup mode (to collect payment method)
73
+ // This will create a subscription with usage-based billing
74
+ const session = await stripe.checkout.sessions.create({
75
+ customer: customerId,
76
+ mode: "subscription",
77
+ payment_method_types: ["card"],
78
+ line_items: [
79
+ {
80
+ price: USAGE_BASED_PRICE_ID,
81
+ },
82
+ ],
83
+ subscription_data: {
84
+ metadata: {
85
+ workspaceId: workspace._id,
86
+ type: "usage_based",
87
+ },
88
+ },
89
+ metadata: {
90
+ workspaceId: workspace._id,
91
+ type: "usage_based",
92
+ },
93
+ success_url: `${appUrl}/workspace?billing=success`,
94
+ cancel_url: `${appUrl}/workspace?billing=cancel`,
95
+ allow_promotion_codes: true,
96
+ });
97
+
98
+ return NextResponse.json({
99
+ url: session.url,
100
+ sessionId: session.id,
101
+ });
102
+ } catch (error) {
103
+ console.error("[Checkout] Error:", error);
104
+ return NextResponse.json(
105
+ { error: error instanceof Error ? error.message : "Checkout failed" },
106
+ { status: 500 }
107
+ );
108
+ }
109
+ }
@@ -0,0 +1,118 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import Stripe from "stripe";
3
+
4
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
5
+ const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
6
+
7
+ const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null;
8
+
9
+ export async function POST(req: NextRequest) {
10
+ if (!stripe) {
11
+ return NextResponse.json({ error: "Stripe not configured" }, { status: 500 });
12
+ }
13
+
14
+ try {
15
+ const { token } = await req.json();
16
+
17
+ if (!token) {
18
+ return NextResponse.json({ error: "Missing token" }, { status: 400 });
19
+ }
20
+
21
+ // Verify session and get workspace
22
+ const sessionRes = await fetch(`${CONVEX_URL}/api/query`, {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({
26
+ path: "workspaces:getSessionWorkspace",
27
+ args: { token },
28
+ }),
29
+ });
30
+
31
+ const sessionData = await sessionRes.json();
32
+ const workspace = sessionData.value || sessionData;
33
+
34
+ if (!workspace || !workspace._id) {
35
+ return NextResponse.json({ error: "Invalid session" }, { status: 401 });
36
+ }
37
+
38
+ if (!workspace.stripeCustomerId) {
39
+ return NextResponse.json({ paymentMethod: null });
40
+ }
41
+
42
+ // Get customer's default payment method
43
+ const customer = await stripe.customers.retrieve(workspace.stripeCustomerId, {
44
+ expand: ["invoice_settings.default_payment_method"],
45
+ });
46
+
47
+ if (customer.deleted) {
48
+ return NextResponse.json({ paymentMethod: null });
49
+ }
50
+
51
+ const defaultPm = customer.invoice_settings?.default_payment_method;
52
+
53
+ if (!defaultPm || typeof defaultPm === "string") {
54
+ // Try to get from subscriptions
55
+ const subscriptions = await stripe.subscriptions.list({
56
+ customer: workspace.stripeCustomerId,
57
+ status: "active",
58
+ limit: 1,
59
+ expand: ["data.default_payment_method"],
60
+ });
61
+
62
+ if (subscriptions.data.length > 0) {
63
+ const subPm = subscriptions.data[0].default_payment_method;
64
+ if (subPm && typeof subPm !== "string" && subPm.type === "card" && subPm.card) {
65
+ return NextResponse.json({
66
+ paymentMethod: {
67
+ brand: subPm.card.brand,
68
+ last4: subPm.card.last4,
69
+ expMonth: subPm.card.exp_month,
70
+ expYear: subPm.card.exp_year,
71
+ },
72
+ });
73
+ }
74
+ }
75
+
76
+ // Try to get any payment method
77
+ const paymentMethods = await stripe.paymentMethods.list({
78
+ customer: workspace.stripeCustomerId,
79
+ type: "card",
80
+ limit: 1,
81
+ });
82
+
83
+ if (paymentMethods.data.length > 0 && paymentMethods.data[0].card) {
84
+ const pm = paymentMethods.data[0];
85
+ return NextResponse.json({
86
+ paymentMethod: {
87
+ brand: pm.card!.brand,
88
+ last4: pm.card!.last4,
89
+ expMonth: pm.card!.exp_month,
90
+ expYear: pm.card!.exp_year,
91
+ },
92
+ });
93
+ }
94
+
95
+ return NextResponse.json({ paymentMethod: null });
96
+ }
97
+
98
+ // Extract card details from default payment method
99
+ if (defaultPm.type === "card" && defaultPm.card) {
100
+ return NextResponse.json({
101
+ paymentMethod: {
102
+ brand: defaultPm.card.brand,
103
+ last4: defaultPm.card.last4,
104
+ expMonth: defaultPm.card.exp_month,
105
+ expYear: defaultPm.card.exp_year,
106
+ },
107
+ });
108
+ }
109
+
110
+ return NextResponse.json({ paymentMethod: null });
111
+ } catch (error) {
112
+ console.error("[Payment Method] Error:", error);
113
+ return NextResponse.json(
114
+ { error: error instanceof Error ? error.message : "Failed to get payment method" },
115
+ { status: 500 }
116
+ );
117
+ }
118
+ }
@@ -0,0 +1,64 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import Stripe from "stripe";
3
+
4
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
5
+ const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
6
+
7
+ const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null;
8
+
9
+ export async function POST(req: NextRequest) {
10
+ if (!stripe) {
11
+ return NextResponse.json({ error: "Stripe not configured" }, { status: 500 });
12
+ }
13
+
14
+ try {
15
+ const { token } = await req.json();
16
+
17
+ if (!token) {
18
+ return NextResponse.json({ error: "Missing token" }, { status: 400 });
19
+ }
20
+
21
+ // Verify session and get workspace
22
+ const sessionRes = await fetch(`${CONVEX_URL}/api/query`, {
23
+ method: "POST",
24
+ headers: { "Content-Type": "application/json" },
25
+ body: JSON.stringify({
26
+ path: "workspaces:getSessionWorkspace",
27
+ args: { token },
28
+ }),
29
+ });
30
+
31
+ const sessionData = await sessionRes.json();
32
+ const workspace = sessionData.value || sessionData;
33
+
34
+ if (!workspace || !workspace._id) {
35
+ return NextResponse.json({ error: "Invalid session" }, { status: 401 });
36
+ }
37
+
38
+ if (!workspace.stripeCustomerId) {
39
+ return NextResponse.json(
40
+ { error: "No billing account. Add a payment method first." },
41
+ { status: 400 }
42
+ );
43
+ }
44
+
45
+ // Determine the app URL for redirects
46
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.com";
47
+
48
+ // Create Billing Portal session
49
+ const session = await stripe.billingPortal.sessions.create({
50
+ customer: workspace.stripeCustomerId,
51
+ return_url: `${appUrl}/workspace?tab=settings&portal=success`,
52
+ });
53
+
54
+ return NextResponse.json({
55
+ url: session.url,
56
+ });
57
+ } catch (error) {
58
+ console.error("[Billing Portal] Error:", error);
59
+ return NextResponse.json(
60
+ { error: error instanceof Error ? error.message : "Failed to create portal session" },
61
+ { status: 500 }
62
+ );
63
+ }
64
+ }
@@ -4,7 +4,7 @@ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-av
4
4
 
5
5
  export async function POST(req: NextRequest) {
6
6
  try {
7
- const { email, fingerprint } = await req.json();
7
+ const { email, fingerprint, referralCode } = await req.json();
8
8
 
9
9
  if (!email || !email.includes("@")) {
10
10
  return NextResponse.json({ error: "Valid email required" }, { status: 400 });
@@ -33,8 +33,11 @@ export async function POST(req: NextRequest) {
33
33
  throw new Error("Failed to get token from Convex");
34
34
  }
35
35
 
36
- // Send magic link email via n8n
37
- const magicLinkUrl = `${process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.nordsym.com"}/dashboard/verify?token=${token}`;
36
+ // Build magic link URL with optional referral code
37
+ let magicLinkUrl = `${process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.nordsym.com"}/auth/verify?token=${token}`;
38
+ if (referralCode) {
39
+ magicLinkUrl += `&ref=${encodeURIComponent(referralCode)}`;
40
+ }
38
41
 
39
42
  await fetch("https://nordsym.app.n8n.cloud/webhook/symbot-gmail", {
40
43
  method: "POST",
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
4
4
  import { useSearchParams } from "next/navigation";
5
5
  import { Suspense } from "react";
6
6
 
7
- const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://brilliant-puffin-712.eu-west-1.convex.cloud";
7
+ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
8
8
 
9
9
  interface VerifyResult {
10
10
  success: boolean;
@@ -23,6 +23,7 @@ interface VerifyResult {
23
23
  function VerifyContent() {
24
24
  const searchParams = useSearchParams();
25
25
  const token = searchParams.get("token");
26
+ const referralCode = searchParams.get("ref"); // Referral code from signup URL
26
27
 
27
28
  const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
28
29
  const [error, setError] = useState<string>("");
@@ -43,17 +44,23 @@ function VerifyContent() {
43
44
  return;
44
45
  }
45
46
 
46
- verifyToken(token);
47
- }, [token]);
47
+ verifyToken(token, referralCode || undefined);
48
+ }, [token, referralCode]);
48
49
 
49
- async function verifyToken(token: string) {
50
+ async function verifyToken(token: string, refCode?: string) {
50
51
  try {
52
+ // Build args with optional referral code
53
+ const args: { token: string; referralCode?: string } = { token };
54
+ if (refCode) {
55
+ args.referralCode = refCode;
56
+ }
57
+
51
58
  const response = await fetch(`${CONVEX_URL}/api/mutation`, {
52
59
  method: "POST",
53
60
  headers: { "Content-Type": "application/json" },
54
61
  body: JSON.stringify({
55
62
  path: "workspaces:verifyMagicLink",
56
- args: { token },
63
+ args,
57
64
  }),
58
65
  });
59
66
 
@@ -68,6 +75,15 @@ function VerifyContent() {
68
75
  setStatus("success");
69
76
  setWorkspace(data.workspace || null);
70
77
  setSessionToken(data.sessionToken || "");
78
+
79
+ // Store session for workspace dashboard
80
+ if (data.sessionToken) {
81
+ localStorage.setItem("apiclaw_workspace_session", data.sessionToken);
82
+ // Auto-redirect to workspace after 2 seconds
83
+ setTimeout(() => {
84
+ window.location.href = "/workspace";
85
+ }, 2000);
86
+ }
71
87
  } else {
72
88
  setStatus("error");
73
89
  switch (data.error) {
@@ -235,9 +251,15 @@ function VerifyContent() {
235
251
  </div>
236
252
  )}
237
253
 
238
- <div className="text-center pt-4 border-t border-[var(--border)]">
254
+ <div className="text-center pt-4 border-t border-[var(--border)] space-y-3">
255
+ <a
256
+ href="/workspace"
257
+ className="inline-flex items-center justify-center px-6 py-3 bg-[var(--accent)] text-white rounded-lg font-medium hover:opacity-90 transition w-full"
258
+ >
259
+ Go to Workspace →
260
+ </a>
239
261
  <p className="text-[var(--text-muted)] text-sm">
240
- You can close this tab now
262
+ Or close this tab if using CLI
241
263
  </p>
242
264
  </div>
243
265
  </div>
@@ -267,8 +289,8 @@ function VerifyContent() {
267
289
  {/* Footer */}
268
290
  <p className="text-center text-[var(--text-muted)] text-xs mt-6">
269
291
  Having trouble? Contact{" "}
270
- <a href="mailto:support@apiclaw.com" className="text-[var(--accent)] hover:underline">
271
- support@apiclaw.com
292
+ <a href="mailto:support_apiclaw@nordsym.com" className="text-[var(--accent)] hover:underline">
293
+ support_apiclaw@nordsym.com
272
294
  </a>
273
295
  </p>
274
296
  </div>
@@ -311,7 +311,7 @@ export default function DocsPage() {
311
311
  </li>
312
312
  <li>
313
313
  <strong>Email:</strong>{' '}
314
- <a href="mailto:symbot@nordsym.com" className="text-[var(--accent)] hover:underline">symbot@nordsym.com</a>
314
+ <a href="mailto:support_apiclaw@nordsym.com" className="text-[var(--accent)] hover:underline">support_apiclaw@nordsym.com</a>
315
315
  </li>
316
316
  </ul>
317
317
  </div>
@@ -10,7 +10,7 @@ const EARN_CHANNELS = [
10
10
  iconName: 'star',
11
11
  title: 'Star on GitHub',
12
12
  description: 'Show some love on GitHub',
13
- credits: 500,
13
+ credits: 20,
14
14
  cta: 'Star Repository',
15
15
  href: 'https://github.com/nordsym/apiclaw',
16
16
  color: 'from-red-500/20 to-orange-500/10',
@@ -22,7 +22,7 @@ const EARN_CHANNELS = [
22
22
  iconName: 'twitter',
23
23
  title: 'Follow @NordSym',
24
24
  description: 'Stay updated on X/Twitter',
25
- credits: 250,
25
+ credits: 15,
26
26
  cta: 'Follow Us',
27
27
  href: 'https://x.com/NordSym',
28
28
  color: 'from-red-400/20 to-rose-500/10',
@@ -34,7 +34,7 @@ const EARN_CHANNELS = [
34
34
  iconName: 'mail',
35
35
  title: 'Join Newsletter',
36
36
  description: 'Get weekly updates & tips',
37
- credits: 100,
37
+ credits: 15,
38
38
  cta: 'Subscribe',
39
39
  href: '#newsletter',
40
40
  color: 'from-rose-500/20 to-red-500/10',
@@ -45,8 +45,8 @@ const EARN_CHANNELS = [
45
45
  id: 'referral',
46
46
  iconName: 'users',
47
47
  title: 'Invite Friends',
48
- description: 'Earn for each friend who joins',
49
- credits: 500,
48
+ description: 'Earn 10 calls for each friend',
49
+ credits: 10,
50
50
  perUnit: 'per friend',
51
51
  cta: 'Copy Link',
52
52
  color: 'from-orange-500/20 to-red-500/10',
@@ -72,7 +72,7 @@ export default function EarnPage() {
72
72
  const [copied, setCopied] = useState(false);
73
73
  const [email, setEmail] = useState('');
74
74
  const [subscribed, setSubscribed] = useState(false);
75
- const [totalCredits] = useState(1350); // Demo value
75
+ const [totalCredits] = useState(50); // Demo value - matches free tier max earn
76
76
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
77
77
 
78
78
  useEffect(() => {
@@ -0,0 +1,49 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { useSearchParams, useRouter } from "next/navigation";
5
+ import { Suspense } from "react";
6
+
7
+ function JoinContent() {
8
+ const searchParams = useSearchParams();
9
+ const router = useRouter();
10
+ const refCode = searchParams.get("ref");
11
+
12
+ useEffect(() => {
13
+ // Store referral code in localStorage for use during signup
14
+ if (refCode) {
15
+ localStorage.setItem("apiclaw_referral_code", refCode);
16
+ }
17
+
18
+ // Redirect to login page
19
+ router.push("/login");
20
+ }, [refCode, router]);
21
+
22
+ return (
23
+ <div className="min-h-screen bg-[var(--background)] flex items-center justify-center p-4">
24
+ <div className="text-center">
25
+ <span className="text-6xl animate-bounce">🦞</span>
26
+ <p className="mt-4 text-[var(--text-secondary)]">
27
+ {refCode ? "Processing your invite..." : "Redirecting to login..."}
28
+ </p>
29
+ </div>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ export default function JoinPage() {
35
+ return (
36
+ <Suspense
37
+ fallback={
38
+ <div className="min-h-screen bg-[var(--background)] flex items-center justify-center">
39
+ <div className="text-center">
40
+ <span className="text-6xl">🦞</span>
41
+ <p className="mt-4 text-[var(--text-secondary)]">Loading...</p>
42
+ </div>
43
+ </div>
44
+ }
45
+ >
46
+ <JoinContent />
47
+ </Suspense>
48
+ );
49
+ }
@@ -48,10 +48,13 @@ export default function LoginPage() {
48
48
  setError(null);
49
49
 
50
50
  try {
51
+ // Check for stored referral code (from /join page)
52
+ const referralCode = localStorage.getItem("apiclaw_referral_code");
53
+
51
54
  const response = await fetch("/api/workspace-auth/magic-link", {
52
55
  method: "POST",
53
56
  headers: { "Content-Type": "application/json" },
54
- body: JSON.stringify({ email }),
57
+ body: JSON.stringify({ email, referralCode }),
55
58
  });
56
59
 
57
60
  if (!response.ok) {
@@ -59,6 +62,9 @@ export default function LoginPage() {
59
62
  throw new Error(data.error || "Failed to send magic link");
60
63
  }
61
64
 
65
+ // Clear referral code after sending (it will be in the email link)
66
+ localStorage.removeItem("apiclaw_referral_code");
67
+
62
68
  setIsSent(true);
63
69
  } catch (err) {
64
70
  setError(err instanceof Error ? err.message : "Something went wrong");
@@ -175,7 +181,7 @@ export default function LoginPage() {
175
181
  <div className="space-y-3 text-sm text-[var(--text-muted)]">
176
182
  <div className="flex items-center gap-2">
177
183
  <Check className="w-4 h-4 text-green-500" />
178
- <span>1,000 free API calls per month</span>
184
+ <span>50 free API calls per month</span>
179
185
  </div>
180
186
  <div className="flex items-center gap-2">
181
187
  <Check className="w-4 h-4 text-green-500" />