From a6e445b318c6a93c55ad5a1185d17244cd144f86 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Sat, 18 Apr 2026 13:35:33 +0200 Subject: [PATCH 1/5] docs(specs): clarify kiloclaw compliance rules --- .specs/kiloclaw-billing.md | 29 +++++++++++++++++++++-------- .specs/kiloclaw-datamodel.md | 11 ++++++++--- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.specs/kiloclaw-billing.md b/.specs/kiloclaw-billing.md index 5c3fe1124..9a3a9a203 100644 --- a/.specs/kiloclaw-billing.md +++ b/.specs/kiloclaw-billing.md @@ -135,10 +135,19 @@ notifications at each stage. ### Payment Sources +The rules in this section govern paid self-service KiloClaw +subscription rows. Trial rows are temporary bootstrap rows and are +exempt from the paid funding invariants in rules 2 and 3. Current +organizational bootstrap rows that grant temporary `managed-active` +access before org billing launches are also outside these funding +invariants; they remain a temporary carveout until org billing +integration ships. + 1. The system MUST record a payment source for each subscription. The value MUST be either `stripe` or `credits`. -2. The system MUST enforce exactly three valid combinations of payment - source and payment provider subscription ID: +2. For paid self-service rows, the system MUST enforce exactly three + valid combinations of payment source and payment provider + subscription ID: | State | payment_source | provider subscription ID | | ------------- | -------------- | ------------------------ | @@ -152,8 +161,9 @@ notifications at each stage. (hybrid) or a null one (pure credit). No other combination is valid. -3. A subscription with payment source `credits` MUST record a credit - renewal timestamp indicating when the next credit deduction is due. +3. A paid self-service subscription with payment source `credits` + MUST record a credit renewal timestamp indicating when the next + credit deduction is due. 4. At most one subscription record per instance is allowed regardless of payment source (see Plans rule 5). 5. User-initiated switching between payment sources is not supported @@ -981,10 +991,13 @@ rows renew. ### User Data Deletion -1. When a user is soft-deleted, the system MUST delete all subscription - records for that user. -2. When a user is soft-deleted, the system MUST delete all email - notification log entries for that user. +1. When a user is soft-deleted, the system MUST retain + `kiloclaw_instance` and `kiloclaw_subscription` rows for that + user. Ownership references and directly identifying user fields + MUST be anonymized rather than deleted. +2. When a user is soft-deleted, the system MUST delete auxiliary + KiloClaw billing records whose purpose is operational rather than + canonical state, such as email notification log entries. 3. Credit transaction records created by subscription deductions are managed by the credit system's own data deletion rules, not by KiloClaw billing. This spec does not impose additional deletion diff --git a/.specs/kiloclaw-datamodel.md b/.specs/kiloclaw-datamodel.md index 31996dfa2..097b661d8 100644 --- a/.specs/kiloclaw-datamodel.md +++ b/.specs/kiloclaw-datamodel.md @@ -242,9 +242,14 @@ complete). instance record is persisted. This call MUST occur as part of the same provisioning request — the window between instance commit and subscription creation (see rule 4) MUST be bounded - to the duration of that request. If subscription creation - fails, the provisioning service MUST retry or mark the instance - as requiring remediation so the orphan is not silently ignored. + to the duration of that request. If the primary subscription + bootstrap path fails after the instance row is persisted, the + provisioning service MUST retry or run a fallback path that + still creates canonical subscription state before the request + exits. The request MUST NOT complete successfully while leaving + a silently unpaired instance row. If both primary and fallback + bootstrap fail, the instance MUST be marked for explicit + remediation rather than left as an unnoticed orphan. 23. The onboarding flow MUST NOT be considered complete (and MUST NOT play the completion "ding" sound) until both the instance record and the subscription record have been persisted to the database. From 75a701237530b8a39188a6fc03e935e8b0bfda88 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Sat, 18 Apr 2026 14:31:14 +0200 Subject: [PATCH 2/5] docs(specs): allow kiloclaw promo codes --- .specs/kiloclaw-billing.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.specs/kiloclaw-billing.md b/.specs/kiloclaw-billing.md index 9a3a9a203..4b90dfa84 100644 --- a/.specs/kiloclaw-billing.md +++ b/.specs/kiloclaw-billing.md @@ -296,9 +296,12 @@ rules resolve conflicts. customer before creating a new checkout session, to guard against concurrent checkouts. This check does not cover provider-side subscriptions in past-due status. -4. The system MUST NOT allow promotional codes for either plan. -5. The system MUST apply a provider-configured first-month discount - coupon when creating a standard plan checkout session. +4. The system MUST allow promotional codes for either plan. +5. The system MUST apply the configured first-month discount when + creating a standard plan checkout session without consuming the + promotional-code input. The implementation MAY use a dedicated + intro price or another provider-supported mechanism that keeps + user-entered promotional codes available. 6. When a configurable billing start date is set and is in the future, the system MUST create the subscription with a delayed billing period that begins on that date. @@ -1084,7 +1087,8 @@ Previous values: New values: - Trial duration: 7 days (existing trials keep their original end date) -- Standard plan: $9/month with $4 first month via coupon, no promotional codes +- Standard plan: $9/month with $4 first month while still allowing + promotional codes - Commit plan: $48/6 months - Trial expiry warning: 2 days before expiry - 14 existing subscribers migrated to new pricing at next billing cycle From 130fabf38b1fae9305932d3c9697e3bb5b98e189 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Sat, 18 Apr 2026 14:54:44 +0200 Subject: [PATCH 3/5] docs(specs): clarify gdpr and remediation exceptions --- .specs/kiloclaw-billing.md | 8 ++++++-- .specs/kiloclaw-datamodel.md | 25 ++++++++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.specs/kiloclaw-billing.md b/.specs/kiloclaw-billing.md index 4b90dfa84..88766038a 100644 --- a/.specs/kiloclaw-billing.md +++ b/.specs/kiloclaw-billing.md @@ -998,10 +998,14 @@ rows renew. `kiloclaw_instance` and `kiloclaw_subscription` rows for that user. Ownership references and directly identifying user fields MUST be anonymized rather than deleted. -2. When a user is soft-deleted, the system MUST delete auxiliary +2. When a user is soft-deleted, the system MUST retain subscription + change-log rows as canonical audit history. Any directly + identifying actor or ownership fields in those rows MUST be + anonymized while preserving the audit trail's meaning. +3. When a user is soft-deleted, the system MUST delete auxiliary KiloClaw billing records whose purpose is operational rather than canonical state, such as email notification log entries. -3. Credit transaction records created by subscription deductions are +4. Credit transaction records created by subscription deductions are managed by the credit system's own data deletion rules, not by KiloClaw billing. This spec does not impose additional deletion requirements on credit transaction records. diff --git a/.specs/kiloclaw-datamodel.md b/.specs/kiloclaw-datamodel.md index 097b661d8..cc2f84839 100644 --- a/.specs/kiloclaw-datamodel.md +++ b/.specs/kiloclaw-datamodel.md @@ -104,6 +104,10 @@ on application logs that may be rotated or incomplete. 3. When a user account is deleted (e.g., GDPR right-to-erasure), instance and subscription records MUST be retained. Ownership references MUST be anonymized rather than cascaded or removed. + Subscription change log rows MUST also be retained as canonical + audit history. Any directly identifying fields in those rows MUST + be anonymized under the GDPR exception in Subscription Change Log + rule 14. Foreign key constraints on these tables MUST NOT cascade deletes from parent tables. @@ -115,7 +119,12 @@ on application logs that may be rotated or incomplete. the instance INSERT and the subscription INSERT where the instance has no subscription. Outside that bounded creation window, there MUST NOT exist an instance record without a - subscription record. This invariant is enforced at the + subscription record, except an instance explicitly quarantined + for bootstrap remediation after both primary and fallback + subscription-bootstrap paths failed (rule 22). That exception + MUST be rare, MUST cause the provisioning request to fail, and + MUST NOT be treated as a live provisioned instance for user + access or onboarding completion. This invariant is enforced at the application layer; the creation-order rules define the sequence that satisfies it. 5. Each subscription record MUST reference exactly one instance. The @@ -191,8 +200,11 @@ and serves as the authoritative audit trail for subscription state. g. An optional context or reason string providing additional detail (e.g., `stripe_invoice:inv_xxx`, `insufficient_credits`, `user_requested`, `trial_expired`). -14. Change log entries MUST NOT be updated or deleted. The log is - strictly append-only. +14. Change log entries MUST NOT be updated or deleted during normal + operation. The log is strictly append-only. GDPR-required + anonymization of directly identifying fields is the sole + exception. That anonymization MUST preserve the event's audit + meaning, timestamps, action labels, and non-identifying context. 15. When the change log entry is written in the same database transaction as the mutation, a change log failure that aborts the transaction is acceptable — the entire operation will be @@ -248,8 +260,11 @@ complete). still creates canonical subscription state before the request exits. The request MUST NOT complete successfully while leaving a silently unpaired instance row. If both primary and fallback - bootstrap fail, the instance MUST be marked for explicit - remediation rather than left as an unnoticed orphan. + bootstrap fail, the provisioning request MUST fail and the + instance MUST be explicitly quarantined for remediation rather + than left as an unnoticed orphan. This quarantine state is the + sole temporary exception to rule 4 and MUST NOT be surfaced as a + successful provisioned instance. 23. The onboarding flow MUST NOT be considered complete (and MUST NOT play the completion "ding" sound) until both the instance record and the subscription record have been persisted to the database. From 73d6f913b8b2cbecf8e29c53a64d5e61523b35a1 Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Sat, 18 Apr 2026 15:01:06 +0200 Subject: [PATCH 4/5] docs(specs): clarify pricing parity scope --- .specs/kiloclaw-billing.md | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/.specs/kiloclaw-billing.md b/.specs/kiloclaw-billing.md index 88766038a..293a3d4ac 100644 --- a/.specs/kiloclaw-billing.md +++ b/.specs/kiloclaw-billing.md @@ -118,8 +118,11 @@ notifications at each stage. 5. The system MUST enforce at most one subscription record per instance. Each subscription MUST reference the instance it funds. A user MAY have multiple instances, each with its own subscription. -6. The user-visible price for each plan MUST be identical regardless - of payment source. +6. The base user-visible price for each plan MUST be identical + regardless of payment source. Payment-provider-native promotions, + coupons, or other checkout-side adjustments are excluded from this + parity rule and are governed by the payment-source-specific rules + below. 7. Stripe-funded billing MUST use configured payment-provider price identifiers. Credit-funded billing MUST use internal microdollar amounts that correspond to the same plan prices. @@ -127,11 +130,14 @@ notifications at each stage. configuration for the selected plan is missing. For Stripe-funded billing this includes the payment-provider price identifier. 9. Each plan MUST support two payment sources: payment-provider - (Stripe) and credits. Plan pricing, access rules, failure handling, - and suspension/destruction timelines MUST be identical regardless - of payment source. The payment mechanism and the internal - implementation of plan switching and cancellation differ by payment - source (see Plan Switching and Cancellation and Reactivation). + (Stripe) and credits. Base plan pricing, built-in first-period + pricing defined by this spec, access rules, failure handling, and + suspension/destruction timelines MUST be identical regardless of + payment source. Payment-provider-native promotions, coupons, and + checkout-side adjustments MAY differ by payment source. The payment + mechanism and the internal implementation of plan switching and + cancellation differ by payment source (see Plan Switching and + Cancellation and Reactivation). ### Payment Sources @@ -296,7 +302,10 @@ rules resolve conflicts. customer before creating a new checkout session, to guard against concurrent checkouts. This check does not cover provider-side subscriptions in past-due status. -4. The system MUST allow promotional codes for either plan. +4. The system MUST allow payment-provider promotional codes for either + plan. These promotions are payment-provider-native checkout + adjustments and do not require an equivalent user-entered mechanism + in the credit-enrollment flow. 5. The system MUST apply the configured first-month discount when creating a standard plan checkout session without consuming the promotional-code input. The implementation MAY use a dedicated @@ -332,10 +341,11 @@ rules resolve conflicts. 2. The system MUST allow credit enrollment when the existing subscription status is trialing or canceled. 3. The system MUST apply a first-month discounted price when enrolling - in the standard plan via credits, identical to the Stripe-configured - first-month discount (see Subscription Checkout rule 5). The - discounted cost is 4,000,000 microdollars. A user qualifies for the - discount when no prior paid subscription exists; a canceled trial + in the standard plan via credits, identical to the built-in Stripe + first-month discount defined in Subscription Checkout rule 5. This + rule does not attempt to mirror user-entered payment-provider promo + codes. The discounted cost is 4,000,000 microdollars. A user + qualifies for the discount when no prior paid subscription exists; a canceled trial subscription (plan = 'trial') MUST NOT count as a prior paid subscription. When the user has a canceled non-trial subscription, the system MUST charge the regular standard price of 9,000,000 @@ -512,7 +522,7 @@ rows renew. 4. The deduction amount MUST equal the settled invoice amount. The system MUST NOT substitute locally defined plan cost constants. Payment-provider-side adjustments (first-month discounts, - prorations) flow through as-is. + promotional codes, coupons, prorations) flow through as-is. 5. Settlement MUST be idempotent. Processing the same invoice twice MUST NOT produce duplicate credits or duplicate deductions. 6. On successful settlement the system MUST: From b8cd17cd8beb56f9e030d198ccb73b708f58200c Mon Sep 17 00:00:00 2001 From: Jean du Plessis Date: Sat, 18 Apr 2026 15:07:19 +0200 Subject: [PATCH 5/5] fix(kiloclaw): block retrial after destroyed instance --- apps/web/src/routers/kiloclaw-billing-router.test.ts | 4 ++-- apps/web/src/routers/kiloclaw-router.ts | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/web/src/routers/kiloclaw-billing-router.test.ts b/apps/web/src/routers/kiloclaw-billing-router.test.ts index 0c2009147..8d25e4495 100644 --- a/apps/web/src/routers/kiloclaw-billing-router.test.ts +++ b/apps/web/src/routers/kiloclaw-billing-router.test.ts @@ -368,7 +368,7 @@ describe('getBillingStatus', () => { expect(result.trialEligible).toBe(true); }); - it('returns trialEligible true when user only has a destroyed personal instance row', async () => { + it('returns trialEligible false when user only has a destroyed personal instance row', async () => { await db.insert(kiloclaw_instances).values({ user_id: user.id, sandbox_id: 'sandbox-destroyed', @@ -379,7 +379,7 @@ describe('getBillingStatus', () => { const result = await caller.kiloclaw.getBillingStatus(); expect(result).not.toBeNull(); - expect(result.trialEligible).toBe(true); + expect(result.trialEligible).toBe(false); }); it('returns trialEligible false when user has an active personal instance row', async () => { diff --git a/apps/web/src/routers/kiloclaw-router.ts b/apps/web/src/routers/kiloclaw-router.ts index e057216f4..298fd7ee4 100644 --- a/apps/web/src/routers/kiloclaw-router.ts +++ b/apps/web/src/routers/kiloclaw-router.ts @@ -1141,7 +1141,7 @@ async function getPersonalBillingStatus(user: { }; } - const [anySubscription, anyPersonalInstance] = await Promise.all([ + const [anySubscription, anyPersonalInstanceHistory] = await Promise.all([ db .select({ id: kiloclaw_subscriptions.id }) .from(kiloclaw_subscriptions) @@ -1152,11 +1152,7 @@ async function getPersonalBillingStatus(user: { .select({ id: kiloclaw_instances.id }) .from(kiloclaw_instances) .where( - and( - eq(kiloclaw_instances.user_id, user.id), - isNull(kiloclaw_instances.organization_id), - isNull(kiloclaw_instances.destroyed_at) - ) + and(eq(kiloclaw_instances.user_id, user.id), isNull(kiloclaw_instances.organization_id)) ) .limit(1) .then(rows => rows[0] ?? null), @@ -1189,7 +1185,7 @@ async function getPersonalBillingStatus(user: { return { hasAccess, accessReason, - trialEligible: !anyPersonalInstance && !anySubscription, + trialEligible: !anyPersonalInstanceHistory && !anySubscription, creditBalanceMicrodollars, creditIntroEligible, hasActiveKiloPass,