@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
@@ -1,133 +0,0 @@
1
- # APIClaw Outreach Targets
2
-
3
- > API providers att kontakta för listing i APIClaw registry
4
- > Senast uppdaterad: 2026-06-17
5
-
6
- ## 🇸🇪 Sverige / Norden — PRIORITET 1
7
-
8
- ### SMS & Communications
9
- | Företag | Kategori | API Docs | Kontakt | Status |
10
- |---------|----------|----------|---------|--------|
11
- | **46elks** | SMS, Voice | https://46elks.com/docs | hello@46elks.com | ⬜ Pending |
12
- | **Sinch** | SMS, Voice, Video | https://developers.sinch.com/ | - | ⬜ Pending |
13
- | **Link Mobility** | SMS, RCS | https://linkmobility.com/developers | - | ⬜ Pending |
14
-
15
- ### Payments & Banking
16
- | Företag | Kategori | API Docs | Kontakt | Status |
17
- |---------|----------|----------|---------|--------|
18
- | **Klarna** | BNPL, Payments | https://docs.klarna.com/ | - | ⬜ Pending |
19
- | **Trustly** | Bank payments | https://developers.trustly.com/ | - | ⬜ Pending |
20
- | **Tink** | Open Banking | https://docs.tink.com/ | - | ⬜ Pending |
21
- | **Open Payments** | PSD2, Banking | https://docs.openpayments.io/ | - | ⬜ Pending |
22
- | **Neonomics** | Open Banking | https://developer.neonomics.io/ | - | ⬜ Pending |
23
- | **Nordea API Market** | Banking | https://developer.nordeaopenbanking.com/ | - | ⬜ Pending |
24
- | **Yapily** | Open Banking | https://docs.yapily.com/ | - | ⬜ Pending |
25
-
26
- ### Identity & Verification
27
- | Företag | Kategori | API Docs | Kontakt | Status |
28
- |---------|----------|----------|---------|--------|
29
- | **BankID** | eID | https://www.bankid.com/utvecklare | - | ⬜ Pending |
30
- | **Freja eID** | eID | https://org.frejaeid.com/developers | - | ⬜ Pending |
31
- | **BehavioSec** | Behavioral biometrics | - | - | ⬜ Pending |
32
-
33
- ---
34
-
35
- ## 🌍 Internationella — PRIORITET 2
36
-
37
- ### SMS & Communications
38
- | Företag | Kategori | API Docs | Kontakt | Status |
39
- |---------|----------|----------|---------|--------|
40
- | **Twilio** | SMS, Voice, Video | https://www.twilio.com/docs | - | ⬜ Pending |
41
- | **Plivo** | SMS, Voice | https://www.plivo.com/docs/ | - | ⬜ Pending |
42
- | **Infobip** | Omnichannel | https://www.infobip.com/docs | - | ⬜ Pending |
43
- | **MessageBird (Bird)** | SMS, Email | https://developers.messagebird.com/ | - | ⬜ Pending |
44
- | **Telnyx** | SMS, Voice | https://developers.telnyx.com/ | - | ⬜ Pending |
45
- | **Vonage** | Communications | https://developer.vonage.com/ | - | ⬜ Pending |
46
-
47
- ### Email
48
- | Företag | Kategori | API Docs | Kontakt | Status |
49
- |---------|----------|----------|---------|--------|
50
- | **SendGrid** | Transactional email | https://docs.sendgrid.com/ | - | ⬜ Pending |
51
- | **Mailgun** | Transactional email | https://documentation.mailgun.com/ | - | ⬜ Pending |
52
- | **Postmark** | Transactional email | https://postmarkapp.com/developer | - | ⬜ Pending |
53
- | **Mailjet** | Email | https://dev.mailjet.com/ | - | ⬜ Pending |
54
- | **Resend** | Dev-first email | https://resend.com/docs | - | ⬜ Pending |
55
- | **Amazon SES** | Email | https://docs.aws.amazon.com/ses/ | - | ⬜ Pending |
56
-
57
- ### Payments
58
- | Företag | Kategori | API Docs | Kontakt | Status |
59
- |---------|----------|----------|---------|--------|
60
- | **Stripe** | Payments | https://stripe.com/docs/api | - | ⬜ Pending |
61
- | **PayPal** | Payments | https://developer.paypal.com/ | - | ⬜ Pending |
62
- | **Adyen** | Payments | https://docs.adyen.com/ | - | ⬜ Pending |
63
- | **Square** | Payments, POS | https://developer.squareup.com/ | - | ⬜ Pending |
64
- | **Paddle** | SaaS billing | https://developer.paddle.com/ | - | ⬜ Pending |
65
- | **Lemon Squeezy** | SaaS billing | https://docs.lemonsqueezy.com/ | - | ⬜ Pending |
66
-
67
- ### Search
68
- | Företag | Kategori | API Docs | Kontakt | Status |
69
- |---------|----------|----------|---------|--------|
70
- | **Algolia** | Search-as-a-service | https://www.algolia.com/doc/ | - | ⬜ Pending |
71
- | **Meilisearch** | Open-source search | https://www.meilisearch.com/docs | - | ⬜ Pending |
72
- | **Typesense** | Search | https://typesense.org/docs/ | - | ⬜ Pending |
73
- | **Elasticsearch** | Search/Analytics | https://www.elastic.co/guide/ | - | ⬜ Pending |
74
-
75
- ### AI & Machine Learning
76
- | Företag | Kategori | API Docs | Kontakt | Status |
77
- |---------|----------|----------|---------|--------|
78
- | **OpenAI** | LLM | https://platform.openai.com/docs | - | ⬜ Pending |
79
- | **Anthropic** | LLM | https://docs.anthropic.com/ | - | ⬜ Pending |
80
- | **Cohere** | NLP/Embeddings | https://docs.cohere.com/ | - | ⬜ Pending |
81
- | **Replicate** | ML models | https://replicate.com/docs | - | ⬜ Pending |
82
- | **Hugging Face** | ML models | https://huggingface.co/docs | - | ⬜ Pending |
83
-
84
- ### Storage & CDN
85
- | Företag | Kategori | API Docs | Kontakt | Status |
86
- |---------|----------|----------|---------|--------|
87
- | **Cloudflare** | CDN, Workers | https://developers.cloudflare.com/ | - | ⬜ Pending |
88
- | **Cloudinary** | Media management | https://cloudinary.com/documentation | - | ⬜ Pending |
89
- | **Imgix** | Image CDN | https://docs.imgix.com/ | - | ⬜ Pending |
90
- | **Uploadcare** | File uploads | https://uploadcare.com/docs/ | - | ⬜ Pending |
91
-
92
- ### Analytics & Data
93
- | Företag | Kategori | API Docs | Kontakt | Status |
94
- |---------|----------|----------|---------|--------|
95
- | **Mixpanel** | Product analytics | https://developer.mixpanel.com/ | - | ⬜ Pending |
96
- | **Amplitude** | Product analytics | https://www.docs.developers.amplitude.com/ | - | ⬜ Pending |
97
- | **Segment** | CDP | https://segment.com/docs/ | - | ⬜ Pending |
98
- | **PostHog** | Open-source analytics | https://posthog.com/docs | - | ⬜ Pending |
99
-
100
- ### Auth & Identity
101
- | Företag | Kategori | API Docs | Kontakt | Status |
102
- |---------|----------|----------|---------|--------|
103
- | **Auth0** | Identity | https://auth0.com/docs | - | ⬜ Pending |
104
- | **Clerk** | Auth for devs | https://clerk.com/docs | - | ⬜ Pending |
105
- | **Stytch** | Passwordless auth | https://stytch.com/docs | - | ⬜ Pending |
106
- | **WorkOS** | Enterprise SSO | https://workos.com/docs | - | ⬜ Pending |
107
-
108
- ---
109
-
110
- ## 📊 Status Legend
111
- - ⬜ Pending — Inte kontaktad
112
- - 📧 Sent — Email skickat
113
- - 👀 Viewed — Öppnat/läst
114
- - 💬 Replied — Svar mottaget
115
- - ✅ Listed — Listad i APIClaw
116
- - ❌ Declined — Avböjt
117
-
118
- ---
119
-
120
- ## 📝 Notes
121
-
122
- ### Prioritering
123
- 1. **Svenska/Nordiska först** — närmare relation, enklare pitch
124
- 2. **Dev-fokuserade APIs** — förstår värdet av discoverability
125
- 3. **Mindre/nyare APIs** — hungrigare på exposure
126
-
127
- ### Kontakt-strategi
128
- - Hitta developer relations / partnerships email
129
- - Alternativt: support@ eller info@
130
- - LinkedIn outreach som backup
131
-
132
- ### Tracking
133
- Uppdatera status här ELLER synka till Airtable (TBD).
@@ -1,146 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Parse public-apis README and convert to APIClaw registry format
4
- """
5
- import re
6
- import json
7
- import hashlib
8
-
9
- # Read the README content
10
- with open('/tmp/public-apis-readme.md', 'r') as f:
11
- content = f.read()
12
-
13
- apis = []
14
- current_category = None
15
-
16
- # Split by lines
17
- lines = content.split('\n')
18
-
19
- for i, line in enumerate(lines):
20
- # Check for category headers (### Animals, ### Anime, etc.)
21
- if line.startswith('### ') and not line.startswith('### API'):
22
- current_category = line[4:].strip()
23
- continue
24
-
25
- # Skip non-table rows
26
- if not line.startswith('|'):
27
- continue
28
-
29
- # Skip header rows and separator rows
30
- if '---' in line or 'API | Description' in line or 'API|Description' in line:
31
- continue
32
-
33
- # Parse table row
34
- # Format: | [Name](url) | Description | Auth | HTTPS | CORS |
35
- parts = line.split('|')
36
- if len(parts) < 5:
37
- continue
38
-
39
- # Extract API info
40
- api_cell = parts[1].strip() if len(parts) > 1 else ''
41
- desc_cell = parts[2].strip() if len(parts) > 2 else ''
42
- auth_cell = parts[3].strip() if len(parts) > 3 else ''
43
- https_cell = parts[4].strip() if len(parts) > 4 else ''
44
- cors_cell = parts[5].strip() if len(parts) > 5 else ''
45
-
46
- # Extract name and link from markdown link format [Name](url)
47
- link_match = re.match(r'\[([^\]]+)\]\(([^)]+)\)', api_cell)
48
- if not link_match:
49
- continue
50
-
51
- name = link_match.group(1)
52
- link = link_match.group(2)
53
-
54
- # Generate slug ID
55
- slug = re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-')
56
-
57
- # Parse auth type
58
- auth = 'None'
59
- if '`apiKey`' in auth_cell or 'apiKey' in auth_cell:
60
- auth = 'apiKey'
61
- elif '`OAuth`' in auth_cell or 'OAuth' in auth_cell:
62
- auth = 'OAuth'
63
- elif '`X-Mashape-Key`' in auth_cell:
64
- auth = 'apiKey'
65
- elif '`User-Agent`' in auth_cell:
66
- auth = 'User-Agent'
67
- elif 'No' in auth_cell:
68
- auth = 'None'
69
-
70
- # Parse HTTPS
71
- https = 'Yes' in https_cell
72
-
73
- # Parse CORS
74
- cors = 'unknown'
75
- if 'Yes' in cors_cell:
76
- cors = 'yes'
77
- elif 'No' in cors_cell:
78
- cors = 'no'
79
-
80
- # Generate keywords from category and description
81
- keywords = []
82
- if current_category:
83
- keywords.append(current_category.lower().replace(' & ', '-').replace(' ', '-'))
84
-
85
- # Add some common keywords from description
86
- desc_lower = desc_cell.lower()
87
- for keyword in ['data', 'api', 'free', 'real-time', 'json', 'rest', 'graphql']:
88
- if keyword in desc_lower:
89
- keywords.append(keyword)
90
-
91
- api_entry = {
92
- "id": slug,
93
- "name": name,
94
- "description": desc_cell,
95
- "category": current_category or "Uncategorized",
96
- "auth": auth,
97
- "https": https,
98
- "cors": cors,
99
- "link": link,
100
- "pricing": "unknown", # public-apis doesn't have pricing info
101
- "keywords": list(set(keywords))
102
- }
103
-
104
- apis.append(api_entry)
105
-
106
- # Remove duplicates by ID (keep first occurrence)
107
- seen_ids = set()
108
- unique_apis = []
109
- for api in apis:
110
- if api['id'] not in seen_ids:
111
- seen_ids.add(api['id'])
112
- unique_apis.append(api)
113
- else:
114
- # Make ID unique by appending category
115
- cat_slug = re.sub(r'[^a-z0-9]+', '-', api['category'].lower()).strip('-')
116
- new_id = f"{api['id']}-{cat_slug}"
117
- if new_id not in seen_ids:
118
- api['id'] = new_id
119
- seen_ids.add(new_id)
120
- unique_apis.append(api)
121
-
122
- print(f"Parsed {len(unique_apis)} unique APIs")
123
-
124
- # Write to JSON
125
- output = {
126
- "version": "1.0.0",
127
- "source": "https://github.com/public-apis/public-apis",
128
- "lastUpdated": "2026-02-16",
129
- "count": len(unique_apis),
130
- "apis": unique_apis
131
- }
132
-
133
- with open('/Users/gustavhemmingsson/clawd/products/api-discovery/src/registry/apis.json', 'w') as f:
134
- json.dump(output, f, indent=2)
135
-
136
- print(f"Wrote to apis.json")
137
-
138
- # Print some stats
139
- categories = {}
140
- for api in unique_apis:
141
- cat = api['category']
142
- categories[cat] = categories.get(cat, 0) + 1
143
-
144
- print("\nAPIs per category:")
145
- for cat, count in sorted(categories.items(), key=lambda x: -x[1]):
146
- print(f" {cat}: {count}")
package/src/revenuecat.ts DELETED
@@ -1,239 +0,0 @@
1
- // RevenueCat integration for APIClaw
2
- // Handles Pro subscription checking and fee calculation
3
-
4
- const REVENUECAT_API_URL = 'https://api.revenuecat.com/v1';
5
- const PROJECT_ID = '0d074df4';
6
-
7
- // Entitlement identifiers
8
- export const ENTITLEMENTS = {
9
- PRO: 'pro',
10
- } as const;
11
-
12
- // Product identifiers
13
- export const PRODUCTS = {
14
- PRO_MONTHLY: 'apiclaw_pro_monthly',
15
- } as const;
16
-
17
- // Fee structure
18
- const FEES = {
19
- FREE: 0.05, // 5% transaction fee
20
- PRO: 0.02, // 2% transaction fee
21
- } as const;
22
-
23
- interface RevenueCatSubscriber {
24
- subscriber: {
25
- entitlements: Record<string, {
26
- product_identifier: string;
27
- expires_date: string | null;
28
- purchase_date: string;
29
- }>;
30
- subscriptions: Record<string, {
31
- expires_date: string | null;
32
- purchase_date: string;
33
- product_plan_identifier?: string;
34
- billing_issues_detected_at?: string | null;
35
- unsubscribe_detected_at?: string | null;
36
- }>;
37
- non_subscriptions: Record<string, unknown>;
38
- first_seen: string;
39
- original_app_user_id: string;
40
- };
41
- }
42
-
43
- /**
44
- * Get RevenueCat API key from environment
45
- */
46
- function getApiKey(): string {
47
- const key = process.env.REVENUECAT_SECRET_KEY;
48
- if (!key) {
49
- throw new Error('REVENUECAT_SECRET_KEY not set');
50
- }
51
- return key;
52
- }
53
-
54
- /**
55
- * Fetch subscriber info from RevenueCat
56
- */
57
- async function getSubscriber(userId: string): Promise<RevenueCatSubscriber | null> {
58
- try {
59
- const response = await fetch(
60
- `${REVENUECAT_API_URL}/subscribers/${encodeURIComponent(userId)}`,
61
- {
62
- headers: {
63
- 'Authorization': `Bearer ${getApiKey()}`,
64
- 'Content-Type': 'application/json',
65
- },
66
- }
67
- );
68
-
69
- if (response.status === 404) {
70
- return null; // User doesn't exist in RevenueCat
71
- }
72
-
73
- if (!response.ok) {
74
- throw new Error(`RevenueCat API error: ${response.status}`);
75
- }
76
-
77
- return await response.json() as RevenueCatSubscriber;
78
- } catch (error) {
79
- console.error('Error fetching subscriber:', error);
80
- return null;
81
- }
82
- }
83
-
84
- /**
85
- * Check if a user has Pro subscription
86
- */
87
- export async function hasProSubscription(userId: string): Promise<boolean> {
88
- const subscriber = await getSubscriber(userId);
89
-
90
- if (!subscriber) {
91
- return false;
92
- }
93
-
94
- const proEntitlement = subscriber.subscriber.entitlements[ENTITLEMENTS.PRO];
95
-
96
- if (!proEntitlement) {
97
- return false;
98
- }
99
-
100
- // Check if entitlement is active (not expired)
101
- if (proEntitlement.expires_date) {
102
- const expiresAt = new Date(proEntitlement.expires_date);
103
- if (expiresAt < new Date()) {
104
- return false;
105
- }
106
- }
107
-
108
- return true;
109
- }
110
-
111
- /**
112
- * Get all entitlements for a user
113
- */
114
- export async function getEntitlements(userId: string): Promise<{
115
- pro: boolean;
116
- expiresAt: string | null;
117
- purchaseDate: string | null;
118
- }> {
119
- const subscriber = await getSubscriber(userId);
120
-
121
- const result = {
122
- pro: false,
123
- expiresAt: null as string | null,
124
- purchaseDate: null as string | null,
125
- };
126
-
127
- if (!subscriber) {
128
- return result;
129
- }
130
-
131
- const proEntitlement = subscriber.subscriber.entitlements[ENTITLEMENTS.PRO];
132
-
133
- if (proEntitlement) {
134
- const expiresAt = proEntitlement.expires_date
135
- ? new Date(proEntitlement.expires_date)
136
- : null;
137
-
138
- result.pro = !expiresAt || expiresAt > new Date();
139
- result.expiresAt = proEntitlement.expires_date;
140
- result.purchaseDate = proEntitlement.purchase_date;
141
- }
142
-
143
- return result;
144
- }
145
-
146
- /**
147
- * Get fee percentage based on subscription status
148
- * Pro = 2%, Free = 5%
149
- */
150
- export async function getFeePercentage(userId: string): Promise<number> {
151
- const isPro = await hasProSubscription(userId);
152
- return isPro ? FEES.PRO : FEES.FREE;
153
- }
154
-
155
- /**
156
- * Calculate platform fee for a transaction
157
- */
158
- export async function calculateFee(userId: string, amountUsd: number): Promise<{
159
- feePercentage: number;
160
- feeAmount: number;
161
- netAmount: number;
162
- isPro: boolean;
163
- }> {
164
- const isPro = await hasProSubscription(userId);
165
- const feePercentage = isPro ? FEES.PRO : FEES.FREE;
166
- const feeAmount = amountUsd * feePercentage;
167
-
168
- return {
169
- feePercentage,
170
- feeAmount: Math.round(feeAmount * 100) / 100, // Round to 2 decimals
171
- netAmount: Math.round((amountUsd - feeAmount) * 100) / 100,
172
- isPro,
173
- };
174
- }
175
-
176
- /**
177
- * Create or update subscriber attributes
178
- */
179
- export async function setSubscriberAttributes(
180
- userId: string,
181
- attributes: Record<string, string>
182
- ): Promise<boolean> {
183
- try {
184
- const formattedAttributes: Record<string, { value: string }> = {};
185
- for (const [key, value] of Object.entries(attributes)) {
186
- formattedAttributes[`$${key}`] = { value };
187
- }
188
-
189
- const response = await fetch(
190
- `${REVENUECAT_API_URL}/subscribers/${encodeURIComponent(userId)}/attributes`,
191
- {
192
- method: 'POST',
193
- headers: {
194
- 'Authorization': `Bearer ${getApiKey()}`,
195
- 'Content-Type': 'application/json',
196
- },
197
- body: JSON.stringify({ attributes: formattedAttributes }),
198
- }
199
- );
200
-
201
- return response.ok;
202
- } catch (error) {
203
- console.error('Error setting subscriber attributes:', error);
204
- return false;
205
- }
206
- }
207
-
208
- /**
209
- * Grant promotional entitlement (for testing/promos)
210
- */
211
- export async function grantPromoEntitlement(
212
- userId: string,
213
- durationDays: number = 30
214
- ): Promise<boolean> {
215
- try {
216
- const response = await fetch(
217
- `${REVENUECAT_API_URL}/subscribers/${encodeURIComponent(userId)}/entitlements/${ENTITLEMENTS.PRO}/promotional`,
218
- {
219
- method: 'POST',
220
- headers: {
221
- 'Authorization': `Bearer ${getApiKey()}`,
222
- 'Content-Type': 'application/json',
223
- },
224
- body: JSON.stringify({
225
- duration: 'daily',
226
- duration_count: durationDays,
227
- }),
228
- }
229
- );
230
-
231
- return response.ok;
232
- } catch (error) {
233
- console.error('Error granting promo entitlement:', error);
234
- return false;
235
- }
236
- }
237
-
238
- // Export types for use elsewhere
239
- export type { RevenueCatSubscriber };
@@ -1,187 +0,0 @@
1
- // RevenueCat webhook handler for APIClaw
2
- // Handles subscription lifecycle events and syncs to Convex
3
-
4
- import { Hono } from 'hono';
5
-
6
- const CONVEX_URL = process.env.CONVEX_URL || 'https://agile-crane-840.convex.cloud';
7
-
8
- // RevenueCat webhook event types
9
- type WebhookEventType =
10
- | 'INITIAL_PURCHASE'
11
- | 'RENEWAL'
12
- | 'CANCELLATION'
13
- | 'UNCANCELLATION'
14
- | 'NON_RENEWING_PURCHASE'
15
- | 'SUBSCRIPTION_PAUSED'
16
- | 'SUBSCRIPTION_EXTENDED'
17
- | 'EXPIRATION'
18
- | 'BILLING_ISSUE'
19
- | 'PRODUCT_CHANGE'
20
- | 'TRANSFER';
21
-
22
- interface RevenueCatWebhookEvent {
23
- api_version: string;
24
- event: {
25
- type: WebhookEventType;
26
- id: string;
27
- app_id: string;
28
- app_user_id: string;
29
- original_app_user_id: string;
30
- aliases: string[];
31
- product_id: string;
32
- entitlement_ids: string[];
33
- period_type: string;
34
- purchased_at_ms: number;
35
- expiration_at_ms: number | null;
36
- environment: 'SANDBOX' | 'PRODUCTION';
37
- store: string;
38
- is_trial_conversion?: boolean;
39
- cancel_reason?: string;
40
- price_in_purchased_currency?: number;
41
- currency?: string;
42
- };
43
- }
44
-
45
- interface UserSubscriptionUpdate {
46
- userId: string;
47
- isPro: boolean;
48
- subscriptionStatus: 'active' | 'cancelled' | 'expired' | 'paused' | 'billing_issue';
49
- productId: string | null;
50
- expiresAt: number | null;
51
- updatedAt: number;
52
- eventType: WebhookEventType;
53
- }
54
-
55
- /**
56
- * Update user subscription status in Convex
57
- */
58
- async function updateConvexUser(update: UserSubscriptionUpdate): Promise<boolean> {
59
- try {
60
- const response = await fetch(`${CONVEX_URL}/api/mutation`, {
61
- method: 'POST',
62
- headers: {
63
- 'Content-Type': 'application/json',
64
- },
65
- body: JSON.stringify({
66
- path: 'users:updateSubscription',
67
- args: update,
68
- }),
69
- });
70
-
71
- if (!response.ok) {
72
- console.error('Convex update failed:', await response.text());
73
- return false;
74
- }
75
-
76
- return true;
77
- } catch (error) {
78
- console.error('Error updating Convex:', error);
79
- return false;
80
- }
81
- }
82
-
83
- /**
84
- * Process webhook event and determine subscription state
85
- */
86
- function processEvent(event: RevenueCatWebhookEvent['event']): UserSubscriptionUpdate {
87
- const now = Date.now();
88
- const expiresAt = event.expiration_at_ms;
89
- const isExpired = expiresAt ? expiresAt < now : false;
90
-
91
- let subscriptionStatus: UserSubscriptionUpdate['subscriptionStatus'] = 'active';
92
- let isPro = true;
93
-
94
- switch (event.type) {
95
- case 'INITIAL_PURCHASE':
96
- case 'RENEWAL':
97
- case 'UNCANCELLATION':
98
- case 'SUBSCRIPTION_EXTENDED':
99
- subscriptionStatus = 'active';
100
- isPro = true;
101
- break;
102
-
103
- case 'CANCELLATION':
104
- // Still active until expiration
105
- subscriptionStatus = 'cancelled';
106
- isPro = !isExpired;
107
- break;
108
-
109
- case 'EXPIRATION':
110
- subscriptionStatus = 'expired';
111
- isPro = false;
112
- break;
113
-
114
- case 'SUBSCRIPTION_PAUSED':
115
- subscriptionStatus = 'paused';
116
- isPro = false;
117
- break;
118
-
119
- case 'BILLING_ISSUE':
120
- subscriptionStatus = 'billing_issue';
121
- // Keep Pro until grace period ends
122
- isPro = !isExpired;
123
- break;
124
-
125
- case 'PRODUCT_CHANGE':
126
- case 'TRANSFER':
127
- subscriptionStatus = 'active';
128
- isPro = event.entitlement_ids.includes('pro');
129
- break;
130
-
131
- default:
132
- isPro = event.entitlement_ids.includes('pro') && !isExpired;
133
- }
134
-
135
- return {
136
- userId: event.app_user_id,
137
- isPro,
138
- subscriptionStatus,
139
- productId: event.product_id,
140
- expiresAt: expiresAt,
141
- updatedAt: now,
142
- eventType: event.type,
143
- };
144
- }
145
-
146
- /**
147
- * Create Hono router for webhook handling
148
- */
149
- export function createWebhookRouter(): Hono {
150
- const app = new Hono();
151
-
152
- // Health check
153
- app.get('/webhooks/revenuecat/health', (c) => {
154
- return c.json({ status: 'ok', service: 'revenuecat-webhook' });
155
- });
156
-
157
- // RevenueCat webhook endpoint
158
- app.post('/webhooks/revenuecat', async (c) => {
159
- try {
160
- const body = await c.req.json<RevenueCatWebhookEvent>();
161
-
162
- console.log(`[RevenueCat] Received ${body.event.type} for user ${body.event.app_user_id}`);
163
-
164
- // Process the event
165
- const update = processEvent(body.event);
166
-
167
- // Update Convex
168
- const success = await updateConvexUser(update);
169
-
170
- if (success) {
171
- console.log(`[RevenueCat] Updated user ${update.userId}: isPro=${update.isPro}`);
172
- return c.json({ success: true });
173
- } else {
174
- return c.json({ success: false, error: 'Failed to update database' }, 500);
175
- }
176
- } catch (error) {
177
- console.error('[RevenueCat] Webhook error:', error);
178
- return c.json({ success: false, error: 'Internal error' }, 500);
179
- }
180
- });
181
-
182
- return app;
183
- }
184
-
185
- // Export for testing
186
- export { processEvent, updateConvexUser };
187
- export type { RevenueCatWebhookEvent, UserSubscriptionUpdate };
File without changes
File without changes