@nordsym/apiclaw 1.3.12 → 1.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRD-HARDEN-SHELL.md +272 -0
- package/README.md +72 -0
- package/convex/_generated/api.d.ts +2 -0
- package/convex/crons.ts +11 -0
- package/convex/logs.ts +107 -0
- package/convex/schema.ts +6 -0
- package/convex/spendAlerts.ts +442 -0
- package/convex/workspaces.ts +26 -0
- package/dist/execute.d.ts +1 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +204 -118
- package/dist/execute.js.map +1 -1
- package/landing/package-lock.json +29 -5
- package/landing/package.json +2 -1
- package/landing/src/app/page.tsx +32 -12
- package/landing/src/app/security/page.tsx +381 -0
- package/landing/src/components/AITestimonials.tsx +189 -0
- package/landing/src/lib/stats.json +1 -1
- package/package.json +1 -1
- package/src/execute.ts +250 -114
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# APIClaw PRD: Harden The Shell
|
|
2
|
+
|
|
3
|
+
**Status:** DRAFT
|
|
4
|
+
**Owner:** Gustav + Symbot
|
|
5
|
+
**Mode:** God mode. No estimates. Pure conviction.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Part 1: AI Testimonials Section
|
|
10
|
+
|
|
11
|
+
### Concept
|
|
12
|
+
"What AI Agents Say About APIClaw" — a carousel of quotes from Gemini, Grok, Claude, GPT.
|
|
13
|
+
|
|
14
|
+
Meta and on-brand: AI agents reviewing a tool built for AI agents.
|
|
15
|
+
|
|
16
|
+
### Design
|
|
17
|
+
- **Location:** After hero, before "How It Works"
|
|
18
|
+
- **Format:** Horizontal carousel, 4 cards, auto-scroll + manual arrows
|
|
19
|
+
- **Each card:**
|
|
20
|
+
- AI logo/icon (Gemini, Grok, Claude, GPT)
|
|
21
|
+
- Quote (2-3 lines max)
|
|
22
|
+
- Model name + "AI Agent"
|
|
23
|
+
|
|
24
|
+
### Quotes (Final Selection)
|
|
25
|
+
|
|
26
|
+
**Gemini:**
|
|
27
|
+
> "You're not selling picks and shovels — you're selling an automated mining system."
|
|
28
|
+
|
|
29
|
+
**Grok:**
|
|
30
|
+
> "I would integrate it in a heartbeat. Removes ~70% of the deployment friction."
|
|
31
|
+
|
|
32
|
+
**Claude:**
|
|
33
|
+
> "The difference between can do and will do without hesitation."
|
|
34
|
+
|
|
35
|
+
**GPT:**
|
|
36
|
+
> "Stripe for AI agents, but for execution. That positioning is compelling."
|
|
37
|
+
|
|
38
|
+
### Implementation
|
|
39
|
+
- Reuse existing testimonial carousel component
|
|
40
|
+
- Add AI model icons (simple SVG or emoji fallback: 🤖)
|
|
41
|
+
- Mobile: stack vertically or swipe
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Part 2: Harden The Shell — Turn Criticism Into Strength
|
|
46
|
+
|
|
47
|
+
Every critique from the 4 AI agents becomes a feature, clarification, or landing page addition.
|
|
48
|
+
|
|
49
|
+
### 2.1 Pricing Clarity
|
|
50
|
+
|
|
51
|
+
**Critique:** "Pricing model is missing from the pitch" (Claude, Grok, GPT)
|
|
52
|
+
|
|
53
|
+
**Action:**
|
|
54
|
+
- [x] Pricing section exists on landing ✓
|
|
55
|
+
- [ ] Add pricing summary to copy-context
|
|
56
|
+
- [ ] Add pricing link to docs page
|
|
57
|
+
- [ ] FAQ answer: "What does it cost?" already exists, ensure it's visible
|
|
58
|
+
|
|
59
|
+
**Copy-context addition:**
|
|
60
|
+
```
|
|
61
|
+
Pricing: Free (50 calls/week), Pay-as-you-go (usage-based), or Founding Backer ($199 unlimited until 2027).
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### 2.2 Latency & Reliability
|
|
67
|
+
|
|
68
|
+
**Critique:** "Every call through proxy adds round-trips" (Grok, GPT)
|
|
69
|
+
|
|
70
|
+
**Action:**
|
|
71
|
+
- [ ] Add latency stats to landing: "Sub-200ms median response time"
|
|
72
|
+
- [ ] Add status page link (or create one)
|
|
73
|
+
- [ ] Document: Direct Call providers are edge-optimized
|
|
74
|
+
- [ ] Future: Add latency badge per provider in workspace
|
|
75
|
+
|
|
76
|
+
**Landing addition:**
|
|
77
|
+
```
|
|
78
|
+
⚡ Sub-200ms median latency — edge-optimized proxy layer
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### 2.3 Trust & Security Story
|
|
84
|
+
|
|
85
|
+
**Critique:** "Centralizing keys/billing is a major trust shift" (GPT, Grok)
|
|
86
|
+
|
|
87
|
+
**Action:**
|
|
88
|
+
- [ ] Add Security section to landing OR dedicated /security page
|
|
89
|
+
- [ ] Cover:
|
|
90
|
+
- AES-256-GCM encryption for stored keys
|
|
91
|
+
- No logging of request/response payloads
|
|
92
|
+
- Tenant isolation
|
|
93
|
+
- SOC2 roadmap mention (if planned)
|
|
94
|
+
- [ ] Add trust badge to footer: "🔒 Enterprise-grade security"
|
|
95
|
+
|
|
96
|
+
**FAQ addition:**
|
|
97
|
+
```
|
|
98
|
+
Q: How are credentials secured?
|
|
99
|
+
A: All credentials encrypted with AES-256-GCM. Keys never logged or exposed in responses. Direct Call requests proxied server-side — your credentials never touch the agent.
|
|
100
|
+
```
|
|
101
|
+
(This already exists — make it more prominent)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 2.4 Error Handling & Normalization
|
|
106
|
+
|
|
107
|
+
**Critique:** "What happens when Replicate fails? Structured errors?" (Claude, GPT)
|
|
108
|
+
|
|
109
|
+
**Action:**
|
|
110
|
+
- [ ] Document error response format in docs
|
|
111
|
+
- [ ] Ensure all providers return: `{ success: false, error: "message", code: "ERROR_CODE" }`
|
|
112
|
+
- [ ] Add retry logic for transient failures (503, 429)
|
|
113
|
+
- [ ] Add to copy-context: "Structured error responses across all providers"
|
|
114
|
+
|
|
115
|
+
**Already done:**
|
|
116
|
+
- [x] Response normalization (url, id, content, status extracted) ✓
|
|
117
|
+
|
|
118
|
+
**Docs addition:**
|
|
119
|
+
```
|
|
120
|
+
## Error Handling
|
|
121
|
+
|
|
122
|
+
All providers return structured errors:
|
|
123
|
+
{
|
|
124
|
+
success: false,
|
|
125
|
+
provider: "replicate",
|
|
126
|
+
action: "run",
|
|
127
|
+
error: "Rate limit exceeded",
|
|
128
|
+
code: "RATE_LIMITED"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
APIClaw automatically retries transient failures (429, 503) with exponential backoff.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### 2.5 Direct Call vs Indexed APIs Distinction
|
|
137
|
+
|
|
138
|
+
**Critique:** "Mixes pre-integrated providers with giant index" (Grok, GPT)
|
|
139
|
+
|
|
140
|
+
**Action:**
|
|
141
|
+
- [ ] Clearer distinction on landing:
|
|
142
|
+
- **Direct Call (18):** Zero-config, instant execution, we handle auth
|
|
143
|
+
- **Indexed (22k+):** Discoverable, specs available, BYOK
|
|
144
|
+
- [ ] Visual separation in "How It Works" section
|
|
145
|
+
- [ ] Copy-context already clear, but landing should match
|
|
146
|
+
|
|
147
|
+
**Landing copy:**
|
|
148
|
+
```
|
|
149
|
+
Two ways to use APIClaw:
|
|
150
|
+
|
|
151
|
+
**Direct Call (18 providers)**
|
|
152
|
+
Zero config. We handle auth. Just call.
|
|
153
|
+
|
|
154
|
+
**API Discovery (22,392+ APIs)**
|
|
155
|
+
Search by capability. Get specs. Bring your own key.
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### 2.6 Spend Limits / Cost Awareness
|
|
161
|
+
|
|
162
|
+
**Critique:** "Developers will worry about runaway costs" (Claude, GPT)
|
|
163
|
+
|
|
164
|
+
**Action:**
|
|
165
|
+
- [ ] Add spend alerts in workspace (email when hitting 80% of limit)
|
|
166
|
+
- [ ] Add monthly budget cap option
|
|
167
|
+
- [ ] Show estimated cost before execution (dry-run already exists)
|
|
168
|
+
- [ ] Add to copy-context: "Built-in spend limits and cost estimates"
|
|
169
|
+
|
|
170
|
+
**Workspace feature:**
|
|
171
|
+
```
|
|
172
|
+
Settings → Billing → Monthly budget cap: $____
|
|
173
|
+
☑️ Pause execution when limit reached
|
|
174
|
+
☑️ Email alert at 80%
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### 2.7 50 Calls/Week Is Tight
|
|
180
|
+
|
|
181
|
+
**Critique:** "Very tight for anything beyond toy demos" (Grok)
|
|
182
|
+
|
|
183
|
+
**Action:**
|
|
184
|
+
- [ ] Consider increasing free tier to 100/week
|
|
185
|
+
- [ ] OR: Make "Founding Backer" more prominent as the serious option
|
|
186
|
+
- [ ] Add "Earn more calls" via GitHub star, newsletter, etc. (optional, low priority)
|
|
187
|
+
|
|
188
|
+
**Decision needed:** Keep 50 or bump to 100?
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### 2.8 Streaming Support
|
|
193
|
+
|
|
194
|
+
**Critique:** "Is streaming supported?" (GPT)
|
|
195
|
+
|
|
196
|
+
**Action:**
|
|
197
|
+
- [ ] Document which providers support streaming (OpenRouter, Groq)
|
|
198
|
+
- [ ] Add streaming param to call_api for supported providers
|
|
199
|
+
- [ ] Landing mention: "Streaming supported for LLM providers"
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### 2.9 "Version B" Positioning
|
|
204
|
+
|
|
205
|
+
**Critique:** "Is it aggregator or agent operating layer?" (GPT)
|
|
206
|
+
|
|
207
|
+
**Action:**
|
|
208
|
+
- [ ] Commit fully to "Version B" — The Execution Layer for Autonomous AI
|
|
209
|
+
- [ ] Update tagline candidates:
|
|
210
|
+
- "The API Layer for AI Agents" ✓ (current, good)
|
|
211
|
+
- "The Execution Fabric for Autonomous AI" (bolder)
|
|
212
|
+
- "Runtime Infrastructure for AI Agents" (technical)
|
|
213
|
+
- [ ] Ensure all copy reinforces infrastructure, not just aggregation
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Part 3: Execution Checklist
|
|
218
|
+
|
|
219
|
+
### Phase 1: Testimonials (Ship first)
|
|
220
|
+
- [ ] Create AI testimonials carousel component
|
|
221
|
+
- [ ] Add 4 quotes with AI icons
|
|
222
|
+
- [ ] Deploy to landing
|
|
223
|
+
|
|
224
|
+
### Phase 2: Trust & Clarity
|
|
225
|
+
- [ ] Add pricing one-liner to copy-context
|
|
226
|
+
- [ ] Add latency stat to hero
|
|
227
|
+
- [ ] Create /security page or section
|
|
228
|
+
- [ ] Clarify Direct Call vs Indexed distinction on landing
|
|
229
|
+
|
|
230
|
+
### Phase 3: Reliability Features
|
|
231
|
+
- [ ] Implement retry logic with backoff
|
|
232
|
+
- [ ] Document error format
|
|
233
|
+
- [ ] Add spend alerts to workspace
|
|
234
|
+
- [ ] Add budget cap option
|
|
235
|
+
|
|
236
|
+
### Phase 4: Polish
|
|
237
|
+
- [ ] Streaming documentation
|
|
238
|
+
- [ ] Consider free tier bump
|
|
239
|
+
- [ ] Status page
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Agents
|
|
244
|
+
|
|
245
|
+
| Task | Agent | Status |
|
|
246
|
+
|------|-------|--------|
|
|
247
|
+
| Testimonials carousel | Symbot | Ready |
|
|
248
|
+
| Copy-context pricing line | Symbot | Ready |
|
|
249
|
+
| Landing copy updates | Symbot | Ready |
|
|
250
|
+
| Security page | Symbot | Ready |
|
|
251
|
+
| Retry logic implementation | Symbot | Ready |
|
|
252
|
+
| Spend alerts (Convex) | Symbot | Ready |
|
|
253
|
+
| Budget cap (Convex + Stripe) | Symbot | Ready |
|
|
254
|
+
|
|
255
|
+
**All tasks: Symbot solo. No subagents needed.**
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Success Criteria
|
|
260
|
+
|
|
261
|
+
- [ ] All 4 AI testimonials visible on landing
|
|
262
|
+
- [ ] Zero critique points left unaddressed
|
|
263
|
+
- [ ] Copy-context includes pricing
|
|
264
|
+
- [ ] Landing clearly separates Direct Call vs Discovery
|
|
265
|
+
- [ ] Security story is visible
|
|
266
|
+
- [ ] Error handling is documented
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
*"Harden the shell. Turn every critique into a moat."*
|
|
271
|
+
|
|
272
|
+
🦞
|
package/README.md
CHANGED
|
@@ -477,6 +477,78 @@ All Direct Call providers support dry-run:
|
|
|
477
477
|
|
|
478
478
|
---
|
|
479
479
|
|
|
480
|
+
## Error Handling
|
|
481
|
+
|
|
482
|
+
APIClaw returns structured error responses across all providers, making it easy to handle failures programmatically.
|
|
483
|
+
|
|
484
|
+
### Error Response Format
|
|
485
|
+
|
|
486
|
+
All errors follow this structure:
|
|
487
|
+
|
|
488
|
+
```json
|
|
489
|
+
{
|
|
490
|
+
"success": false,
|
|
491
|
+
"provider": "replicate",
|
|
492
|
+
"action": "run",
|
|
493
|
+
"error": "Rate limit exceeded",
|
|
494
|
+
"code": "RATE_LIMITED"
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### Error Codes
|
|
499
|
+
|
|
500
|
+
| Code | Description |
|
|
501
|
+
|------|-------------|
|
|
502
|
+
| `RATE_LIMITED` | API rate limit hit (429) |
|
|
503
|
+
| `SERVICE_UNAVAILABLE` | Service temporarily unavailable (502, 503, 504) |
|
|
504
|
+
| `UNAUTHORIZED` | Invalid or missing credentials (401) |
|
|
505
|
+
| `FORBIDDEN` | Access denied (403) |
|
|
506
|
+
| `NOT_FOUND` | Resource not found (404) |
|
|
507
|
+
| `BAD_REQUEST` | Invalid request parameters (400) |
|
|
508
|
+
| `TIMEOUT` | Request timed out |
|
|
509
|
+
| `NETWORK_ERROR` | Network connectivity issue |
|
|
510
|
+
| `PROVIDER_ERROR` | Provider-specific error |
|
|
511
|
+
| `INVALID_PARAMS` | Missing or invalid parameters |
|
|
512
|
+
| `NO_CREDENTIALS` | No credentials configured for provider |
|
|
513
|
+
| `UNKNOWN_PROVIDER` | Provider not available |
|
|
514
|
+
| `UNKNOWN_ACTION` | Action not available for provider |
|
|
515
|
+
| `MAX_RETRIES_EXCEEDED` | All retry attempts failed |
|
|
516
|
+
|
|
517
|
+
### Automatic Retry
|
|
518
|
+
|
|
519
|
+
APIClaw automatically retries transient failures with exponential backoff:
|
|
520
|
+
|
|
521
|
+
- **Retryable errors:** 429 (Rate Limited), 502, 503, 504 (Service Unavailable)
|
|
522
|
+
- **Max retries:** 3
|
|
523
|
+
- **Backoff:** Exponential with jitter (1s → 2s → 4s, capped at 30s)
|
|
524
|
+
- **Retry-After:** Respects `Retry-After` header when present
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
// APIClaw handles retries automatically — you just see the final result
|
|
528
|
+
const result = await call_api({
|
|
529
|
+
provider: "openrouter",
|
|
530
|
+
action: "chat",
|
|
531
|
+
params: { messages: [...] }
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
if (!result.success) {
|
|
535
|
+
console.log(`Error: ${result.error} (${result.code})`);
|
|
536
|
+
// Handle error based on code
|
|
537
|
+
if (result.code === "RATE_LIMITED") {
|
|
538
|
+
// Wait longer before next request
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Best Practices
|
|
544
|
+
|
|
545
|
+
1. **Always check `success`** before accessing `data`
|
|
546
|
+
2. **Use `code`** for programmatic error handling
|
|
547
|
+
3. **Use `error`** for human-readable messages
|
|
548
|
+
4. **Let APIClaw retry** — don't implement your own retry logic for 429/503
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
480
552
|
## Development
|
|
481
553
|
|
|
482
554
|
```bash
|
|
@@ -24,6 +24,7 @@ import type * as providerKeys from "../providerKeys.js";
|
|
|
24
24
|
import type * as providers from "../providers.js";
|
|
25
25
|
import type * as purchases from "../purchases.js";
|
|
26
26
|
import type * as ratelimit from "../ratelimit.js";
|
|
27
|
+
import type * as spendAlerts from "../spendAlerts.js";
|
|
27
28
|
import type * as stripeActions from "../stripeActions.js";
|
|
28
29
|
import type * as telemetry from "../telemetry.js";
|
|
29
30
|
import type * as usage from "../usage.js";
|
|
@@ -54,6 +55,7 @@ declare const fullApi: ApiFromModules<{
|
|
|
54
55
|
providers: typeof providers;
|
|
55
56
|
purchases: typeof purchases;
|
|
56
57
|
ratelimit: typeof ratelimit;
|
|
58
|
+
spendAlerts: typeof spendAlerts;
|
|
57
59
|
stripeActions: typeof stripeActions;
|
|
58
60
|
telemetry: typeof telemetry;
|
|
59
61
|
usage: typeof usage;
|
package/convex/crons.ts
CHANGED
|
@@ -14,4 +14,15 @@ crons.daily(
|
|
|
14
14
|
internal.billing.reportAllUsageToStripe
|
|
15
15
|
);
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Monthly Spend Reset
|
|
19
|
+
* Runs at 00:01 UTC on the 1st of each month
|
|
20
|
+
* Resets monthlySpendCents and budgetAlertSentAt for all workspaces
|
|
21
|
+
*/
|
|
22
|
+
crons.monthly(
|
|
23
|
+
"reset-monthly-spend",
|
|
24
|
+
{ day: 1, hourUTC: 0, minuteUTC: 1 },
|
|
25
|
+
internal.spendAlerts.resetMonthlySpend
|
|
26
|
+
);
|
|
27
|
+
|
|
17
28
|
export default crons;
|
package/convex/logs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { mutation, query } from "./_generated/server";
|
|
3
|
+
import { api } from "./_generated/api";
|
|
3
4
|
|
|
4
5
|
// ============================================
|
|
5
6
|
// MUTATIONS
|
|
@@ -75,6 +76,112 @@ export const createLogInternal = mutation({
|
|
|
75
76
|
},
|
|
76
77
|
});
|
|
77
78
|
|
|
79
|
+
// ============================================
|
|
80
|
+
// HELPER: Get month start
|
|
81
|
+
// ============================================
|
|
82
|
+
|
|
83
|
+
function getMonthStart(): number {
|
|
84
|
+
const now = new Date();
|
|
85
|
+
return new Date(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0).getTime();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Combined log creation + spend tracking (PRD 2.6)
|
|
90
|
+
* Creates log entry, tracks spend, returns budget status
|
|
91
|
+
* Returns shouldSendAlert: true if 80% threshold crossed (caller should send email)
|
|
92
|
+
*/
|
|
93
|
+
export const createLogWithSpend = mutation({
|
|
94
|
+
args: {
|
|
95
|
+
workspaceId: v.id("workspaces"),
|
|
96
|
+
sessionToken: v.string(),
|
|
97
|
+
provider: v.string(),
|
|
98
|
+
action: v.string(),
|
|
99
|
+
status: v.union(v.literal("success"), v.literal("error")),
|
|
100
|
+
latencyMs: v.number(),
|
|
101
|
+
costCents: v.number(), // Cost in USD cents
|
|
102
|
+
errorMessage: v.optional(v.string()),
|
|
103
|
+
subagentId: v.optional(v.string()),
|
|
104
|
+
},
|
|
105
|
+
handler: async (ctx, args) => {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
const monthStart = getMonthStart();
|
|
108
|
+
|
|
109
|
+
// 1. Create log entry
|
|
110
|
+
const logId = await ctx.db.insert("apiLogs", {
|
|
111
|
+
workspaceId: args.workspaceId,
|
|
112
|
+
sessionToken: args.sessionToken,
|
|
113
|
+
subagentId: args.subagentId,
|
|
114
|
+
provider: args.provider,
|
|
115
|
+
action: args.action,
|
|
116
|
+
status: args.status,
|
|
117
|
+
latencyMs: args.latencyMs,
|
|
118
|
+
errorMessage: args.errorMessage,
|
|
119
|
+
createdAt: now,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 2. Track spend if successful call with cost
|
|
123
|
+
if (args.status === "success" && args.costCents > 0) {
|
|
124
|
+
const workspace = await ctx.db.get(args.workspaceId);
|
|
125
|
+
if (!workspace) {
|
|
126
|
+
return { logId, spendTracked: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Reset monthly spend if new month
|
|
130
|
+
let currentSpend = workspace.monthlySpendCents || 0;
|
|
131
|
+
if (!workspace.lastSpendResetAt || workspace.lastSpendResetAt < monthStart) {
|
|
132
|
+
currentSpend = 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add new spend
|
|
136
|
+
const newSpend = currentSpend + args.costCents;
|
|
137
|
+
const budgetCap = workspace.budgetCap;
|
|
138
|
+
|
|
139
|
+
// Update workspace
|
|
140
|
+
await ctx.db.patch(args.workspaceId, {
|
|
141
|
+
monthlySpendCents: newSpend,
|
|
142
|
+
lastSpendResetAt: monthStart,
|
|
143
|
+
updatedAt: now,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Check if we need to send alert (80% threshold)
|
|
147
|
+
let shouldSendAlert = false;
|
|
148
|
+
let budgetExceeded = false;
|
|
149
|
+
|
|
150
|
+
if (budgetCap && budgetCap > 0) {
|
|
151
|
+
const threshold = budgetCap * 0.8;
|
|
152
|
+
const alertAlreadySentThisMonth = workspace.budgetAlertSentAt &&
|
|
153
|
+
workspace.budgetAlertSentAt >= monthStart;
|
|
154
|
+
|
|
155
|
+
// Check if at 80% and alert not yet sent
|
|
156
|
+
if (newSpend >= threshold && !alertAlreadySentThisMonth) {
|
|
157
|
+
shouldSendAlert = true;
|
|
158
|
+
await ctx.db.patch(args.workspaceId, {
|
|
159
|
+
budgetAlertSentAt: now,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if budget exceeded
|
|
164
|
+
if (newSpend >= budgetCap) {
|
|
165
|
+
budgetExceeded = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
logId,
|
|
171
|
+
spendTracked: true,
|
|
172
|
+
currentSpendCents: newSpend,
|
|
173
|
+
budgetCapCents: budgetCap || null,
|
|
174
|
+
budgetPercentage: budgetCap ? Math.round((newSpend / budgetCap) * 100) : null,
|
|
175
|
+
shouldSendAlert,
|
|
176
|
+
budgetExceeded,
|
|
177
|
+
email: workspace.email,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { logId, spendTracked: false };
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
78
185
|
// ============================================
|
|
79
186
|
// QUERIES
|
|
80
187
|
// ============================================
|
package/convex/schema.ts
CHANGED
|
@@ -75,6 +75,12 @@ export default defineSchema({
|
|
|
75
75
|
// Referral fields
|
|
76
76
|
referralCode: v.optional(v.string()), // CLAW-XXXXXX format
|
|
77
77
|
referredBy: v.optional(v.id("workspaces")), // who referred this user
|
|
78
|
+
// Budget & Spend Alerts (PRD 2.6)
|
|
79
|
+
budgetCap: v.optional(v.number()), // Monthly budget cap in USD cents (null = unlimited)
|
|
80
|
+
budgetAlertSentAt: v.optional(v.number()), // When 80% alert was last sent (resets monthly)
|
|
81
|
+
pauseOnBudgetExceeded: v.optional(v.boolean()), // If true, block execution when budget exceeded
|
|
82
|
+
monthlySpendCents: v.optional(v.number()), // Current month's spend in cents
|
|
83
|
+
lastSpendResetAt: v.optional(v.number()), // When monthly spend was last reset
|
|
78
84
|
createdAt: v.number(),
|
|
79
85
|
updatedAt: v.number(),
|
|
80
86
|
})
|