From 9bd20cb2e964fb7d1036584ec9e74558551ee4ce Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:33:52 +0000 Subject: [PATCH] Add changelog entry for checkout_session_params deep-merge fix (#999) * docs improvements * many changes * prompt changes * add refetch comment * fix: checkout subscription data * cus eligibility fixes * rm cursor thing --------- Co-authored-by: Ayush Rodrigues Co-authored-by: John Yeo Co-authored-by: John Yeo <51376134+johnyeocx@users.noreply.github.com> Co-authored-by: Ayush <74830628+ay-rod@users.noreply.github.com> --- apps/docs/mintlify/docs.json | 42 +- .../{ => customers}/edge-cases.mdx | 2 +- .../getting-started/display-billing.mdx | 285 ++++---- .../modelling-pricing/per-unit-pricing.mdx | 101 ++- .../modelling-pricing/prepaid-pricing.mdx | 286 ++++++++ .../modelling-pricing/proration.mdx | 2 +- .../modelling-pricing/recurring.mdx | 54 +- .../modelling-pricing/usage-based-pricing.mdx | 2 +- apps/docs/mintlify/welcome.mdx | 42 +- bun.lock | 2 +- packages/atmn/package.json | 12 + .../atmn/src/lib/hooks/useCreateGuides.ts | 32 +- .../atmn/src/lib/hooks/useCreateSkills.ts | 29 +- packages/atmn/src/prompts/creditSystemDocs.ts | 662 ------------------ packages/atmn/src/prompts/customer.ts | 296 -------- packages/atmn/src/prompts/payments.ts | 254 ------- packages/atmn/src/prompts/prepaidDocs.ts | 463 ------------ packages/atmn/src/prompts/pricing.ts | 149 ---- .../skills/autumn-accepting-payments.ts | 346 --------- .../src/prompts/skills/autumn-billing-page.ts | 514 ++++++++++++++ .../skills/autumn-creating-customers.ts | 334 --------- ...king-metered-usage.ts => autumn-gating.ts} | 197 +++--- .../skills/autumn-modelling-pricing-plans.ts | 533 ++++++++++---- .../atmn/src/prompts/skills/autumn-setup.ts | 340 +++++++++ packages/atmn/src/prompts/skills/index.ts | 41 +- packages/atmn/src/prompts/usage.ts | 131 ---- .../views/react/init/steps/HandoffStep.tsx | 31 +- vite/package.json | 1 + vite/src/components/v2/CodeGroup.tsx | 2 +- vite/src/components/v2/FeatureSelector.tsx | 2 +- vite/src/components/v2/SDKSelector.tsx | 19 +- vite/src/lib/snippets/curlSnippets.ts | 44 +- vite/src/lib/snippets/nodeSnippets.ts | 107 ++- vite/src/lib/snippets/pythonSnippets.ts | 113 ++- vite/src/lib/snippets/reactSnippets.ts | 394 +++++------ vite/src/lib/snippets/registry.ts | 10 +- vite/src/lib/snippets/stackOptionsConfig.tsx | 3 - vite/src/lib/snippets/types.ts | 20 +- vite/src/views/onboarding4/CodeSheet.tsx | 13 +- .../src/views/onboarding4/OnboardingGuide.tsx | 45 +- .../hooks/useOnboardingProgress.tsx | 31 +- .../onboarding4/hooks/usePricingAgentChat.ts | 88 +-- .../views/onboarding4/onboardingPrompts.ts | 186 +---- .../src/views/onboarding4/prompts/customer.md | 294 -------- .../src/views/onboarding4/prompts/payments.md | 248 ------- vite/src/views/onboarding4/prompts/usage.md | 125 ---- vite/tsconfig.json | 3 +- vite/vite.config.ts | 5 + 48 files changed, 2449 insertions(+), 4486 deletions(-) rename apps/docs/mintlify/documentation/{ => customers}/edge-cases.mdx (99%) create mode 100644 apps/docs/mintlify/documentation/modelling-pricing/prepaid-pricing.mdx delete mode 100644 packages/atmn/src/prompts/creditSystemDocs.ts delete mode 100644 packages/atmn/src/prompts/customer.ts delete mode 100644 packages/atmn/src/prompts/payments.ts delete mode 100644 packages/atmn/src/prompts/prepaidDocs.ts delete mode 100644 packages/atmn/src/prompts/pricing.ts delete mode 100644 packages/atmn/src/prompts/skills/autumn-accepting-payments.ts create mode 100644 packages/atmn/src/prompts/skills/autumn-billing-page.ts delete mode 100644 packages/atmn/src/prompts/skills/autumn-creating-customers.ts rename packages/atmn/src/prompts/skills/{autumn-tracking-metered-usage.ts => autumn-gating.ts} (55%) create mode 100644 packages/atmn/src/prompts/skills/autumn-setup.ts delete mode 100644 packages/atmn/src/prompts/usage.ts delete mode 100644 vite/src/views/onboarding4/prompts/customer.md delete mode 100644 vite/src/views/onboarding4/prompts/payments.md delete mode 100644 vite/src/views/onboarding4/prompts/usage.md diff --git a/apps/docs/mintlify/docs.json b/apps/docs/mintlify/docs.json index a53a09ba7..6ad9cc794 100644 --- a/apps/docs/mintlify/docs.json +++ b/apps/docs/mintlify/docs.json @@ -49,7 +49,22 @@ "groups": [ { "group": " ", - "pages": ["welcome", "documentation/getting-started/migration"] + "pages": [ + "welcome", + { + "group": "Concepts", + "pages": [ + "documentation/concepts/overview", + "documentation/concepts/plans", + "documentation/concepts/features", + "documentation/concepts/plan-items", + "documentation/concepts/subscriptions", + "documentation/concepts/balances", + "documentation/concepts/stripe" + ] + }, + "documentation/getting-started/migration" + ] }, { "group": "Getting Started", @@ -60,31 +75,20 @@ ] }, { - "group": "Concepts", - "pages": [ - "documentation/concepts/overview", - "documentation/concepts/plans", - "documentation/concepts/features", - "documentation/concepts/plan-items", - "documentation/concepts/subscriptions", - "documentation/concepts/balances", - "documentation/concepts/stripe" - ] - }, - { - "group": "Modelling Pricing", + "group": "Configure Pricing", "pages": [ "documentation/modelling-pricing/recurring", "documentation/modelling-pricing/one-off-purchases", "documentation/modelling-pricing/free-plans", - "documentation/modelling-pricing/trials", "documentation/modelling-pricing/credit-systems", - "documentation/modelling-pricing/per-unit-pricing", + "documentation/modelling-pricing/prepaid-pricing", "documentation/modelling-pricing/usage-based-pricing", + "documentation/modelling-pricing/per-unit-pricing", "documentation/modelling-pricing/rollovers", "documentation/modelling-pricing/proration", - "documentation/modelling-pricing/spend-limits", + "documentation/modelling-pricing/trials", "documentation/modelling-pricing/auto-top-ups", + "documentation/modelling-pricing/spend-limits", "documentation/modelling-pricing/add-ons", "documentation/modelling-pricing/graduated-pricing", "documentation/modelling-pricing/volume-based-tiers", @@ -100,7 +104,8 @@ "documentation/customers/subscription-lifecycle", "documentation/customers/updating-subscriptions", "documentation/customers/custom-plans", - "documentation/customers/versioning" + "documentation/customers/versioning", + "documentation/customers/edge-cases" ] }, { @@ -118,7 +123,6 @@ { "group": "Additional Resources", "pages": [ - "documentation/edge-cases", "documentation/webhooks", "documentation/external-providers/convex", "documentation/external-providers/revenuecat", diff --git a/apps/docs/mintlify/documentation/edge-cases.mdx b/apps/docs/mintlify/documentation/customers/edge-cases.mdx similarity index 99% rename from apps/docs/mintlify/documentation/edge-cases.mdx rename to apps/docs/mintlify/documentation/customers/edge-cases.mdx index ecc0ca265..641adb052 100644 --- a/apps/docs/mintlify/documentation/edge-cases.mdx +++ b/apps/docs/mintlify/documentation/customers/edge-cases.mdx @@ -1,5 +1,5 @@ --- -title: "Edge Cases" +title: "Billing Reliability" description: "How Autumn handles 3DS, payment failures, and other uncommon states" --- diff --git a/apps/docs/mintlify/documentation/getting-started/display-billing.mdx b/apps/docs/mintlify/documentation/getting-started/display-billing.mdx index 2989b528a..b9dbc1597 100644 --- a/apps/docs/mintlify/documentation/getting-started/display-billing.mdx +++ b/apps/docs/mintlify/documentation/getting-started/display-billing.mdx @@ -7,138 +7,41 @@ Software applications typically ship with a billing page. This allows customers The customer endpoint returns the current state of the customer, including their active subscriptions, one-time purchases, and feature balances. -### Active plans +## Pricing table -Display the plan the user is currently on. Users can have multiple active subscriptions and purchases (e.g., main plan and add-ons). - -- **`subscriptions`** - Free and paid recurring plans -- **`purchases`** - One-off plans (e.g., credit top-ups) - - -```jsx React -import { useCustomer } from "autumn-js/react"; - -const { data: customer } = useCustomer(); - -const active = customer?.subscriptions.filter( - (sub) => sub.status === "active" -); - -console.log(active?.map((sub) => sub.planId).join(", ")); -``` - -```typescript TypeScript -import { Autumn } from "autumn-js"; - -const autumn = new Autumn({ secretKey: "am_sk_test_1234" }); - -const customer = await autumn.customers.getOrCreate({ - customerId: "user_123", -}); - -const active = customer.subscriptions?.filter( - (sub) => sub.status === "active" -); - -console.log(active?.map((sub) => sub.planId).join(", ")); -``` - -```python Python -from autumn_sdk import Autumn - -autumn = Autumn("am_sk_test_1234") - -customer = await autumn.customers.get_or_create( - customer_id="user_123" -) - -active = [s for s in customer.subscriptions if s.status == "active"] -print([s.plan_id for s in active]) -``` - -```bash cURL -curl -X POST 'https://api.useautumn.com/v1/customers' \ - -H 'Authorization: Bearer am_sk_test_1234' \ - -H 'Content-Type: application/json' \ - -d '{ "customer_id": "user_123" }' - -# Response includes subscriptions array -``` - - -### Usage balances - -Metered features have `granted`, `usage`, and `remaining` fields. Use these to display current usage and remaining balance. - - - -```jsx React -import { useCustomer } from "autumn-js/react"; - -const { data: customer } = useCustomer(); - -const messages = customer?.balances.messages; - -console.log(`${messages?.remaining} / ${messages?.granted}`); -``` - -```typescript TypeScript -import { Autumn } from "autumn-js"; - -const autumn = new Autumn({ secretKey: "am_sk_test_1234" }); - -const customer = await autumn.customers.getOrCreate({ - customerId: "user_123", -}); - -const messages = customer.balances?.messages; -console.log(`${messages?.remaining} / ${messages?.granted}`); -``` - -```python Python -from autumn_sdk import Autumn - -autumn = Autumn("am_sk_test_1234") - -customer = await autumn.customers.get_or_create( - customer_id="user_123" -) - -messages = customer.balances.get("messages") -print(f"{messages.remaining} / {messages.granted}") -``` - -```bash cURL -curl -X POST 'https://api.useautumn.com/v1/customers' \ - -H 'Authorization: Bearer am_sk_test_1234' \ - -H 'Content-Type: application/json' \ - -d '{ "customer_id": "user_123" }' - -# Response includes balances.[feature_id] -``` - - -## Billing flows - -### Customer eligibility - -When building a pricing page, you need to know what each plan means for the current customer — is it an upgrade, a downgrade, or their current plan? Is a free trial available? +When building a pricing table, you need to know what each plan means for the current customer — is it an upgrade, a downgrade, or their current plan? Is a free trial available? Pass a `customerId` when listing plans and each plan will include a `customerEligibility` object: -- **`scenario`** — The attach scenario for this customer: `new`, `upgrade`, `downgrade`, `active`, `scheduled`, `cancel`, `expired`, `past_due`, or `renew` -- **`trialAvailable`** — Whether the customer is eligible for the plan's free trial +| Field | Type | Description | +|-------|------|-------------| +| `attachAction` | `"activate"` \| `"upgrade"` \| `"downgrade"` \| `"purchase"` \| `"none"` | What happens when this plan is attached | +| `status` | `"active"` \| `"scheduled"` \| undefined | The customer's current relationship to this plan | +| `trialAvailable` | boolean | Whether the customer is eligible for the plan's free trial | -```jsx React +```jsx React expandable import { useListPlans, useCustomer } from "autumn-js/react"; -const buttonText = { - new: "Get started", +const labels = { + activate: "Subscribe", upgrade: "Upgrade", downgrade: "Downgrade", - active: "Current plan", + purchase: "Purchase", +}; + +const getLabel = (eligibility) => { + + if (eligibility?.attachAction === "none") { + return eligibility.status === "scheduled" ? "Plan Scheduled" : "Current plan"; + } + + if (labels[eligibility?.attachAction]) { + return labels[eligibility.attachAction]; + } + + return "Get started"; }; export default function PricingPage() { @@ -148,10 +51,10 @@ export default function PricingPage() { return plans?.map((plan) => ( )); } @@ -166,10 +69,9 @@ const { list: plans } = await autumn.plans.list({ customerId: "user_123", }); -// Each plan includes customerEligibility.scenario and customerEligibility.trialAvailable for (const plan of plans) { - console.log(plan.name, plan.customerEligibility?.scenario); - // e.g. "Free" "downgrade", "Pro" "active", "Enterprise" "upgrade" + console.log(plan.name, plan.customerEligibility?.attachAction); + // e.g. "Free" "downgrade", "Pro" "none", "Enterprise" "upgrade" } ``` @@ -180,9 +82,8 @@ autumn = Autumn("am_sk_test_1234") plans = await autumn.plans.list(customer_id="user_123") -# Each plan includes customer_eligibility.scenario and customer_eligibility.trial_available for plan in plans.list: - print(plan.name, plan.customer_eligibility.scenario) + print(plan.name, plan.customer_eligibility.attach_action) ``` ```bash cURL @@ -200,7 +101,7 @@ curl -X POST 'https://api.useautumn.com/v1/plans.list' \ The React `useListPlans` hook automatically includes customer context from `AutumnProvider`, so `customerEligibility` is populated on every plan without extra configuration. -### Switching plans +## Switching plans Switching plans uses `billing.attach`. See [Attaching Plans](/documentation/customers/payment-flow) for the full guide. @@ -258,7 +159,7 @@ curl -X POST 'https://api.useautumn.com/v1/attach' \ -### Cancelling a plan +## Cancelling a plan Cancel a subscription using `billing.update` with a `cancelAction`. See [Subscription Lifecycle](/documentation/customers/subscription-lifecycle#cancellations) for the full guide on immediate vs end-of-cycle cancellations. @@ -315,7 +216,7 @@ curl -X POST 'https://api.useautumn.com/v1/billing/update' \ -### Uncancelling a plan +## Uncancelling a plan If a subscription has a pending cancellation, a scheduled downgrade, or a scheduled plan switch, you can reverse it with `cancelAction: "uncancel"`. @@ -402,6 +303,122 @@ curl -X POST 'https://api.useautumn.com/v1/billing/update' \ +## Active plans + +Display the plan the user is currently on. Users can have multiple active subscriptions and purchases (e.g., main plan and add-ons). + +- **`subscriptions`** - Free and paid recurring plans +- **`purchases`** - One-off plans (e.g., credit top-ups) + + +```jsx React +import { useCustomer } from "autumn-js/react"; + +const { data: customer } = useCustomer(); + +const active = customer?.subscriptions.filter( + (sub) => sub.status === "active" +); + +console.log(active?.map((sub) => sub.planId).join(", ")); +``` + +```typescript TypeScript +import { Autumn } from "autumn-js"; + +const autumn = new Autumn({ secretKey: "am_sk_test_1234" }); + +const customer = await autumn.customers.getOrCreate({ + customerId: "user_123", +}); + +const active = customer.subscriptions?.filter( + (sub) => sub.status === "active" +); + +console.log(active?.map((sub) => sub.planId).join(", ")); +``` + +```python Python +from autumn_sdk import Autumn + +autumn = Autumn("am_sk_test_1234") + +customer = await autumn.customers.get_or_create( + customer_id="user_123" +) + +active = [s for s in customer.subscriptions if s.status == "active"] +print([s.plan_id for s in active]) +``` + +```bash cURL +curl -X POST 'https://api.useautumn.com/v1/customers' \ + -H 'Authorization: Bearer am_sk_test_1234' \ + -H 'Content-Type: application/json' \ + -d '{ "customer_id": "user_123" }' + +# Response includes subscriptions array +``` + + +## Usage balances + +Metered features have `granted`, `usage`, and `remaining` fields. Use these to display current usage and remaining balance. + + + +```jsx React +import { useCustomer } from "autumn-js/react"; + +const { data: customer, refetch } = useCustomer(); + +const messages = customer?.balances.messages; + +console.log(`${messages?.remaining} / ${messages?.granted}`); + +// After tracking usage or changing plans, call refetch() to update balances +await refetch(); +``` + +```typescript TypeScript +import { Autumn } from "autumn-js"; + +const autumn = new Autumn({ secretKey: "am_sk_test_1234" }); + +const customer = await autumn.customers.getOrCreate({ + customerId: "user_123", +}); + +const messages = customer.balances?.messages; +console.log(`${messages?.remaining} / ${messages?.granted}`); +``` + +```python Python +from autumn_sdk import Autumn + +autumn = Autumn("am_sk_test_1234") + +customer = await autumn.customers.get_or_create( + customer_id="user_123" +) + +messages = customer.balances.get("messages") +print(f"{messages.remaining} / {messages.granted}") +``` + +```bash cURL +curl -X POST 'https://api.useautumn.com/v1/customers' \ + -H 'Authorization: Bearer am_sk_test_1234' \ + -H 'Content-Type: application/json' \ + -d '{ "customer_id": "user_123" }' + +# Response includes balances.[feature_id] +``` + + + + ## Stripe billing portal The Stripe billing portal lets users manage their payment method, view past invoices, and cancel their plan. @@ -461,7 +478,7 @@ curl -X POST 'https://api.useautumn.com/v1/billing.open_customer_portal' \ -## Usage history chart +## Usage timeseries chart Autumn provides aggregate time series queries for usage data. Pass the response to a charting library like Recharts. @@ -515,3 +532,5 @@ curl -X POST 'https://api.useautumn.com/v1/events.aggregate' \ }' ``` + +You can also use the [`events.list`](/api-reference/events/listEvents) method to get the raw event data and display it in a table. diff --git a/apps/docs/mintlify/documentation/modelling-pricing/per-unit-pricing.mdx b/apps/docs/mintlify/documentation/modelling-pricing/per-unit-pricing.mdx index 0ecaa8ef0..3c2e27f4d 100644 --- a/apps/docs/mintlify/documentation/modelling-pricing/per-unit-pricing.mdx +++ b/apps/docs/mintlify/documentation/modelling-pricing/per-unit-pricing.mdx @@ -63,14 +63,18 @@ Push changes with `atmn push`. ## Billing methods -| Method | Behavior | -|--------|----------| -| **Prepaid** | Customer commits to a quantity at checkout and pays immediately. To change quantity, they update their subscription. | -| **Usage-based** | Customer is billed for the actual number of units at the end of each billing cycle. | +| Method | When charged | Quantity | Best for | +|--------|-------------|----------|----------| +| **Prepaid** | Upfront at purchase | Customer selects a fixed quantity | Seat licenses with committed counts | +| **Usage-based** | End of billing cycle (prorated on changes) | Automatic — tracks actual usage | Seats that fluctuate frequently | ### Prepaid per-unit -With prepaid, the customer selects a quantity when purchasing. Pass the quantity via `options`: +With prepaid, the customer selects a **total quantity** when purchasing. The `quantity` includes any free included amount — Autumn subtracts the included amount and charges for the remainder. + +For example, with 5 included seats at \$10/extra seat, a customer who selects `quantity: 10` gets 10 seats total and pays for 5 extra seats (\$50/month). + +Pass the quantity via `featureQuantities`: @@ -79,11 +83,11 @@ import { Autumn } from "autumn-js"; const autumn = new Autumn({ secretKey: "am_sk_..." }); -const { data } = await autumn.checkout({ - customer_id: "user_123", - plan_id: "pro", - options: [{ - feature_id: "seats", +const { data } = await autumn.billing.attach({ + customerId: "user_123", + planId: "pro", + featureQuantities: [{ + featureId: "seats", quantity: 10, }], }); @@ -94,10 +98,10 @@ from autumn_sdk import Autumn autumn = Autumn("am_sk_...") -response = await autumn.checkout( +response = await autumn.billing.attach( customer_id="user_123", plan_id="pro", - options=[{ + feature_quantities=[{ "feature_id": "seats", "quantity": 10, }], @@ -105,13 +109,13 @@ response = await autumn.checkout( ``` ```bash cURL -curl -X POST "https://api.useautumn.com/v1/checkout" \ +curl -X POST "https://api.useautumn.com/v1/billing/attach" \ -H "Authorization: Bearer am_sk_..." \ -H "Content-Type: application/json" \ -d '{ "customer_id": "user_123", "plan_id": "pro", - "options": [{ + "feature_quantities": [{ "feature_id": "seats", "quantity": 10 }] @@ -120,9 +124,15 @@ curl -X POST "https://api.useautumn.com/v1/checkout" \ +The customer's balance is set to the total quantity (10). If they're upgrading and already have seats in use, the existing usage is carried over — so a customer with 3 seats in use would see a remaining balance of 7. + + +Autumn does not prevent you from passing a `quantity` lower than the customer's current usage. If the customer has 5 seats in use and you pass `quantity: 3`, the balance goes negative (-2). The `check` endpoint will return `allowed: false`, preventing new seats from being added, but existing seats are not forcibly removed. + + ### Usage-based per-unit -With usage-based billing, track seat additions and removals as they happen. Autumn bills the total at the end of the billing cycle. +With usage-based billing, no quantity is needed at purchase time. Track seat additions and removals as they happen, and Autumn bills for the actual number of seats in use. @@ -131,11 +141,19 @@ import { Autumn } from "autumn-js"; const autumn = new Autumn({ secretKey: "am_sk_..." }); +// Add a seat await autumn.track({ customer_id: "user_123", feature_id: "seats", value: 1, }); + +// Remove a seat +await autumn.track({ + customer_id: "user_123", + feature_id: "seats", + value: -1, +}); ``` ```python Python @@ -143,14 +161,23 @@ from autumn_sdk import Autumn autumn = Autumn("am_sk_...") +# Add a seat await autumn.track( customer_id="user_123", feature_id="seats", value=1, ) + +# Remove a seat +await autumn.track( + customer_id="user_123", + feature_id="seats", + value=-1, +) ``` ```bash cURL +# Add a seat curl -X POST "https://api.useautumn.com/v1/track" \ -H "Authorization: Bearer am_sk_..." \ -H "Content-Type: application/json" \ @@ -163,6 +190,42 @@ curl -X POST "https://api.useautumn.com/v1/track" \ +When a customer purchases the plan, any seats already in use are **automatically reflected** in their subscription from day one. For example, if a customer has 3 seats in use and purchases a plan with 5 included seats at \$10/extra seat: + +- Their balance starts at 5 (the included amount) +- The 3 existing seats are carried over, leaving a remaining balance of 2 +- No extra charge yet — they're within the included amount +- As they add seats beyond 5, each additional seat is billed at \$10/month with [proration](/documentation/modelling-pricing/proration) + +## Existing usage on upgrade + +When a customer upgrades from one plan to another, Autumn **automatically carries over** their current seat usage to the new plan. This ensures there's no gap in tracking — existing seats don't disappear or go unbilled. + +### Prepaid + +The customer's balance is set to their chosen quantity. Existing usage is then deducted from that balance. + +> **Example**: Customer has **3 seats** in use. They purchase a plan with 5 included seats, passing `quantity: 10`. +> - Balance is set to 10 (5 included + 5 purchased) +> - 3 existing seats are deducted → **7 remaining** +> - Stripe charges for 10 seats (with 5 in the free tier) + +### Usage-based + +No quantity is needed. The Stripe subscription quantity is set to the customer's current usage automatically. + +> **Example**: Customer has **3 seats** in use. They purchase a plan with 5 included seats at \$10/extra seat. +> - Balance starts at 5 (included amount) +> - 3 existing seats are deducted → **2 remaining** +> - Stripe subscription reflects 3 seats in use (within the free tier, so no extra charge) +> - When they add a 6th seat, billing begins at \$10/seat for the overage + +| Scenario | Prepaid (qty: 8) | Usage-based | +|----------|------------------|-------------| +| **3 in use, 5 included** | Balance: 8 → 5 remaining. Charged for 3 extra. | Balance: 5 → 2 remaining. No extra charge. | +| **3 in use, 0 included** | Balance: 8 → 5 remaining. Charged for 8. | Balance: 0 → -3. Charged for 3 seats. | +| **7 in use, 5 included** | Balance: 8 → 1 remaining. Charged for 3 extra. | Balance: 5 → -2. Charged for 2 extra seats. | + ## Checking access Before allowing a user to add a new seat, check if they have capacity: @@ -176,7 +239,7 @@ const { data } = await autumn.check({ }); if (!data.allowed) { - // Prompt user to purchase more seats + // Prompt user to purchase more seats or upgrade } ``` @@ -187,7 +250,7 @@ response = await autumn.check( ) if not response.allowed: - # Prompt user to purchase more seats + # Prompt user to purchase more seats or upgrade ``` ```bash cURL @@ -202,6 +265,10 @@ curl -X POST "https://api.useautumn.com/v1/check" \ +For **prepaid**, `allowed` is `true` when the customer has remaining prepaid balance (ie. unused seats). + +For **usage-based**, `allowed` is `true` as long as the customer has a usage-based price configured — additional seats are simply billed at the per-unit rate, so there's no hard cap. + ## Proration on quantity changes When a customer increases or decreases their seat count mid-billing-cycle, you can configure how the price adjustment is handled. See [Proration](/documentation/modelling-pricing/proration) for details. diff --git a/apps/docs/mintlify/documentation/modelling-pricing/prepaid-pricing.mdx b/apps/docs/mintlify/documentation/modelling-pricing/prepaid-pricing.mdx new file mode 100644 index 000000000..91b6e836c --- /dev/null +++ b/apps/docs/mintlify/documentation/modelling-pricing/prepaid-pricing.mdx @@ -0,0 +1,286 @@ +--- +title: Prepaid Pricing +description: Charge customers upfront for a quantity of a feature, and draw from it as usage occurs +--- + +Prepaid pricing lets customers pay for a fixed quantity of a feature upfront. They select how many units they want at purchase time, pay immediately, and their balance is decremented as they use it. + +This is in contrast to [usage-based pricing](/documentation/modelling-pricing/usage-based-pricing), where customers are billed for actual usage at the end of a billing cycle. + +> **Example**
+> An AI platform has a Pro plan at \$20/month that includes: +> - **API Credits**: 500 included for free, then \$10 per 1,000 credits per month (consumable) +> - **Seats**: 3 included for free, then \$5 per seat per month (non-consumable) +> +> A customer selects 3,000 credits and 10 seats. They pay \$20 base + \$25 for 2,500 extra credits + \$35 for 7 extra seats = \$80/month. + +## Setting up + + + + +Create your features and add them to a plan with `prepaid` prices: + +```ts autumn.config.ts +import { feature, item, plan } from 'atmn'; + +export const apiCredits = feature({ + id: 'api_credits', + name: 'API Credits', + type: 'metered', + consumable: true, +}); + +export const seats = feature({ + id: 'seats', + name: 'Seats', + type: 'metered', + consumable: false, +}); + +export const pro = plan({ + id: 'pro', + name: 'Pro', + price: { amount: 20, interval: 'month' }, + items: [ + item({ + featureId: apiCredits.id, + included: 500, + price: { + amount: 10, + billingUnits: 1000, + billingMethod: 'prepaid', + interval: 'month', + }, + }), + item({ + featureId: seats.id, + included: 3, + price: { + amount: 5, + billingMethod: 'prepaid', + interval: 'month', + }, + }), + ], +}); +``` + +Push changes with `atmn push`. + + + + +1. Navigate to **Plans** and create or edit a plan +2. Add your features: + - A `metered`, `consumable` feature for credits (e.g., "API Credits") — set an **included** amount (500), a **price** ($10 per 1,000 per month), and billing method **Prepaid** + - A `metered`, `non-consumable` feature for seats (e.g., "Seats") — set an **included** amount (3), a **price** ($5 per seat per month), and billing method **Prepaid** +3. Save the plan + + + + +## How it works + +When a plan has prepaid features, customers select a **quantity** at purchase time. This quantity determines: + +- **How many units are granted** as their balance +- **How much they're charged**, based on the price and billing units + +The `quantity` is the **total** number of feature units the customer will receive, including any included amount. + +Using our example plan: +- A customer selects **3,000 API credits**. 500 are included, so they pay for 2,500 → \$10 × (2,500 / 1,000) = **\$25/month** for credits. +- The same customer selects **10 seats**. 3 are included, so they pay for 7 → \$5 × 7 = **\$35/month** for seats. + + +If you pass a `quantity` equal to or less than the included amount, the customer gets the included amount and pays nothing extra for that feature. + + +## Passing `feature_quantities` + +When attaching a plan or updating a subscription that contains prepaid features, use the `feature_quantities` parameter to specify how many units the customer wants. + +### Attaching a plan + +Pass a `feature_quantities` entry for each prepaid feature on the plan: + + + +```typescript TypeScript +import { Autumn } from "autumn-js"; + +const autumn = new Autumn({ secretKey: "am_sk_..." }); + +const { data } = await autumn.billing.attach({ + customerId: "user_123", + planId: "pro", + featureQuantities: [ + { featureId: "api_credits", quantity: 3000 }, + { featureId: "seats", quantity: 10 }, + ], +}); +``` + +```python Python +from autumn_sdk import Autumn + +autumn = Autumn("am_sk_...") + +response = await autumn.billing.attach( + customer_id="user_123", + plan_id="pro", + feature_quantities=[ + { "feature_id": "api_credits", "quantity": 3000 }, + { "feature_id": "seats", "quantity": 10 }, + ], +) +``` + +```bash cURL +curl -X POST "https://api.useautumn.com/v1/billing/attach" \ + -H "Authorization: Bearer am_sk_..." \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_123", + "plan_id": "pro", + "feature_quantities": [ + { "feature_id": "api_credits", "quantity": 3000 }, + { "feature_id": "seats", "quantity": 10 } + ] + }' +``` + + + +### Updating a subscription + +To change prepaid quantities on an existing subscription, use `billing.update`. For example, to add more seats mid-cycle: + + + +```typescript TypeScript +await autumn.billing.update({ + customerId: "user_123", + planId: "pro", + featureQuantities: [ + { featureId: "api_credits", quantity: 3000 }, + { featureId: "seats", quantity: 15 }, + ], +}); +``` + +```python Python +await autumn.billing.update( + customer_id="user_123", + plan_id="pro", + feature_quantities=[ + { "feature_id": "api_credits", "quantity": 3000 }, + { "feature_id": "seats", "quantity": 15 }, + ], +) +``` + +```bash cURL +curl -X POST "https://api.useautumn.com/v1/billing/update" \ + -H "Authorization: Bearer am_sk_..." \ + -H "Content-Type: application/json" \ + -d '{ + "customer_id": "user_123", + "plan_id": "pro", + "feature_quantities": [ + { "feature_id": "api_credits", "quantity": 3000 }, + { "feature_id": "seats", "quantity": 15 } + ] + }' +``` + + + +See [Updating Subscriptions](/documentation/customers/updating-subscriptions) for more on previewing changes. When quantities change mid-cycle, Autumn can prorate the charge — see [Proration](/documentation/modelling-pricing/proration) for configuration options. + + +## Understanding prepaid balances + +Once a customer is attached to a plan with prepaid features, their balance `breakdown` distinguishes between what was included for free and what was purchased. + +| Field | Description | +|-------|-------------| +| `included_grant` | The amount granted by the plan for free — the "included" amount configured on the plan item. | +| `prepaid_grant` | The amount purchased via `feature_quantities` — the quantity minus the included amount. | +| `granted` | Top-level total: `included_grant + prepaid_grant` summed across all breakdown items. | +| `remaining` | How much is left to use. | +| `usage` | How much has been consumed. | + + +Using the plan from our setup, a customer who attaches with 3,000 credits and 10 seats will have: + +```json expandable +{ + "api_credits": { + "feature_id": "api_credits", + "granted": 3000, + "remaining": 3000, + "usage": 0, + "unlimited": false, + "overage_allowed": false, + "breakdown": [ + { + "id": "cus_ent_abc123", + "plan_id": "pro", + "included_grant": 500, + "prepaid_grant": 2500, + "remaining": 3000, + "usage": 0, + "reset": { + "interval": "month", + "resets_at": 1773851121437 + }, + "price": { + "amount": 10, + "billing_units": 1000, + "billing_method": "prepaid" + }, + "expires_at": null + } + ] + }, + "seats": { + "feature_id": "seats", + "granted": 10, + "remaining": 10, + "usage": 0, + "unlimited": false, + "overage_allowed": false, + "breakdown": [ + { + "id": "cus_ent_def456", + "plan_id": "pro", + "included_grant": 3, + "prepaid_grant": 7, + "remaining": 10, + "usage": 0, + "reset": null, + "price": { + "amount": 5, + "billing_units": 1, + "billing_method": "prepaid" + }, + "expires_at": null + } + ] + } +} +``` + +Use the [check](/documentation/customers/check) endpoint before allowing a customer to use a prepaid feature, and [track](/documentation/customers/tracking-usage) usage afterwards to decrement their balance. + +## Prepaid vs usage-based + +| | Prepaid | Usage-based | +|---|---|---| +| **When charged** | Upfront at purchase | End of billing cycle | +| **Customer selects quantity** | Yes, via `feature_quantities` | No | +| **Balance behavior** | Decremented as usage occurs | Accumulated and billed | +| **Best for** | Credits, top-ups, seat licenses | Metered APIs, storage, bandwidth | + diff --git a/apps/docs/mintlify/documentation/modelling-pricing/proration.mdx b/apps/docs/mintlify/documentation/modelling-pricing/proration.mdx index 1c6cb9790..fccf2748a 100644 --- a/apps/docs/mintlify/documentation/modelling-pricing/proration.mdx +++ b/apps/docs/mintlify/documentation/modelling-pricing/proration.mdx @@ -6,7 +6,7 @@ description: Handle mid-cycle plan changes with prorated billing Proration adjusts billing when a customer changes their subscription mid-cycle — whether upgrading to a higher plan, downgrading, or changing the quantity of a non-consumable feature like seats. Autumn calculates the prorated amount and either charges or credits the customer. > **Example**
-> A customer on a $20/month plan upgrades to a $50/month plan halfway through the billing cycle. They're charged $15 (the prorated difference for the remaining half of the month). +> A customer on a \$20/month plan upgrades to a \$50/month plan halfway through the billing cycle. They're charged \$15 (the prorated difference for the remaining half of the month). ## Setting up diff --git a/apps/docs/mintlify/documentation/modelling-pricing/recurring.mdx b/apps/docs/mintlify/documentation/modelling-pricing/recurring.mdx index 7d36cb338..0503a6b17 100644 --- a/apps/docs/mintlify/documentation/modelling-pricing/recurring.mdx +++ b/apps/docs/mintlify/documentation/modelling-pricing/recurring.mdx @@ -1,12 +1,12 @@ --- title: Recurring Plans -description: Set up recurring subscription plans for your customers +description: Grant customers a recurring allowance of consumable features like messages, credits, or API calls --- -Subscriptions are the most common way to charge customers on a recurring basis. A subscription plan has a fixed base price that customers pay at a regular interval (monthly, quarterly, annually), and can include features with usage limits or additional usage-based charges. +Recurring plans let you grant customers a fixed allowance of consumable features -- like messages, credits, or API calls -- that resets each billing period. Customers pay a base price at a regular interval (monthly, quarterly, annually), and receive a fresh grant of their included features at the start of each cycle. > **Example**
-> A project management tool offers a Pro plan at $20/month that includes 10 seats, 50GB storage, and SSO access. +> An AI writing tool offers a Pro plan at $20/month that grants 1,000 messages per month. When the billing period resets, the customer's message balance is reset back to 1,000. ## Setting up @@ -25,12 +25,6 @@ export const messages = feature({ consumable: true, }); -export const sso = feature({ - id: 'sso', - name: 'SSO', - type: 'boolean', -}); - export const pro = plan({ id: 'pro', name: 'Pro', @@ -41,9 +35,6 @@ export const pro = plan({ included: 1000, reset: { interval: 'month' }, }), - item({ - featureId: sso.id, - }), ], }); ``` @@ -57,7 +48,7 @@ Push changes with `atmn push`. 2. Click **Create Plan** 3. Set a **name** and **ID** for the plan (e.g., "Pro", `pro`) 4. Under **Price**, set the amount and select a billing interval (`month`, `quarter`, `semi_annual`, or `year`) -5. Add features to the plan — set grant amounts, reset intervals, and prices as needed. These will be granted to the customer once they purchase the plan. +5. Add consumable features to the plan -- set grant amounts and reset intervals. These will be granted to the customer each billing period once they subscribe. 6. Save your changes @@ -182,30 +173,6 @@ curl -X POST "https://api.useautumn.com/v1/attach" \ "expiresAt": null } ] - }, - "sso": { - "featureId": "sso", - "granted": 1, - "remaining": 1, - "usage": 0, - "unlimited": false, - "overageAllowed": false, - "maxPurchase": null, - "nextResetAt": null, - "breakdown": [ - { - "id": "cus_ent_def456", - "planId": "pro", - "includedGrant": 1, - "prepaidGrant": 0, - "remaining": 1, - "usage": 0, - "unlimited": false, - "reset": null, - "price": null, - "expiresAt": null - } - ] } } } @@ -215,8 +182,8 @@ curl -X POST "https://api.useautumn.com/v1/attach" \ When a subscription is created, Autumn: 1. Creates a Stripe subscription with the plan's prices -2. Provisions [balances](/documentation/concepts/balances) for each feature in the plan -3. Starts the billing cycle based on the plan's interval +2. Grants the customer their included [balances](/documentation/concepts/balances) for each consumable feature +3. Starts the billing cycle -- balances reset automatically at the start of each period ## Billing intervals @@ -234,6 +201,15 @@ You can create a separate plan for each interval you want to support. For exampl You can also configure a custom `interval_count` to charge at non-standard intervals (e.g., every 2 months). +### Billing interval vs reset interval + +The billing interval (how often the customer is charged) and the reset interval (how often their feature balance replenishes) are configured independently. They don't have to match. + +> **Example**
+> A plan billed at $200/year could grant 100 messages/month. The customer pays once a year, but their message balance resets to 100 every month. + +This is useful when you want to offer an annual discount while still metering usage on a shorter cycle. + ## Managing subscriptions Once a customer has an active subscription, you can manage upgrades, downgrades, and cancellations. See [Managing Subscriptions](/documentation/customers/s) for details on: diff --git a/apps/docs/mintlify/documentation/modelling-pricing/usage-based-pricing.mdx b/apps/docs/mintlify/documentation/modelling-pricing/usage-based-pricing.mdx index 8d855cfc3..a66cb5d5b 100644 --- a/apps/docs/mintlify/documentation/modelling-pricing/usage-based-pricing.mdx +++ b/apps/docs/mintlify/documentation/modelling-pricing/usage-based-pricing.mdx @@ -6,7 +6,7 @@ description: Bill customers based on actual usage at the end of each billing per Pay-per-use (usage-based) pricing charges customers based on how much of a feature they actually consume, billed at the end of each billing period. This is ideal for products where usage varies significantly between customers. > **Example**
-> A notification service charges $1 per 1,000 notifications sent. A customer who sends 5,000 notifications in a month pays $5 at the end of that month. +> A notification service charges \$1 per 1,000 notifications sent. A customer who sends 5,000 notifications in a month pays \$5 at the end of that month. ## Setting up diff --git a/apps/docs/mintlify/welcome.mdx b/apps/docs/mintlify/welcome.mdx index 5bdeae714..ff65bb867 100644 --- a/apps/docs/mintlify/welcome.mdx +++ b/apps/docs/mintlify/welcome.mdx @@ -1,15 +1,17 @@ --- title: Welcome to Autumn sidebarTitle: Introduction -description: "The open source, drop-in system of record for AI and SaaS monetization." +description: "Drop-in, open-source control layer for AI and SaaS monetization." --- ## What is Autumn? -Autumn is a pricing and billing layer between your application and Stripe. It acts as your source of truth for customer subscription statuses, usage metering and credit balances. +Autumn is a pricing and billing layer between your application and Stripe. It owns the subscription lifecycle, credit ledgers, and entitlement state that you'd otherwise build and maintain yourself. Instead of billing logic living in your code and database, your app can query Autumn in real-time to check if a customer is allowed to do something (eg, send an AI message, access SSO, etc). +This saves you months of engineering time, and makes pricing changes a simple configuration change. + ```mermaid actions={false} flowchart TD subgraph app["Your Application"] @@ -43,22 +45,20 @@ flowchart TD ## Why use Autumn? -AI-style monetization is harder for developers to build and maintain. For reference, OpenAI wrote a [blog post](https://openai.com/index/beyond-rate-limits/) about their in-house system. Over time, you will end up building: +For reference, here's what a production-grade monetization system looks like in the AI era. OpenAI wrote a [blog post](https://openai.com/index/beyond-rate-limits/) about their in-house system. | Feature | Requirements | |-----------------------|--------------------------------------------------------------------------------------------------| | Subscription logic | Checkouts, prorated upgrades, scheduled downgrades, add-ons, trials. 10+ webhook cases. | -| Credit system | Real-time enforcement, periodic vs one-time grants, rollovers, expiration, concurrency control | +| Credit ledgers | Real-time enforcement, periodic vs one-time grants, rollovers, expiration, concurrency control | | Controls and observability | Auto top ups, spend caps, per-seat allowances, usage analytics, event logs | | Versioning and grandfathering | Various price IDs, migration scripts, backwards compatibility | | Enterprise and custom plans | Custom code, tiered pricing, custom credit grants, pilots, expansion logic | | Edge cases | Plan switching, monthly/annual changes, failed payments, 3DS, race conditions, refunds | -At some point you or your GTM team will want to change your pricing, and you will need to rebuild everything. +Billing starts with a simple checkout flow, and balloons in complexity as you add more features and scale. And when you want to change your pricing, you need to rebuild everything. Yet, it's a critical part of your product that you cannot afford to get wrong. -Autumn replaces this, and offloads all this logic out of your codebase. After you set it up, everything about your pricing can be managed through our dashboard or config file. - -It's faster to setup, more flexible, more reliable and saves teams months of engineering time. +You can choose to build this yourself, or use Autumn to offload all this logic out of your codebase. It's less work, more flexible, and more reliable. @@ -68,29 +68,18 @@ It's faster to setup, more flexible, more reliable and saves teams months of eng ## How is this different? -Stripe has its own metered billing product, and there are others out there too like Metronome, Orb, Lago etc. All of these products allow you to record and bill for usage after an action is taken, making them good for end of month invoicing. +Most billing tools, including Stripe's built-in metering, are designed for post-hoc invoicing: you send usage events, they generate invoices at end of period. Your app still owns who gets access, when limits apply, how downgrades work (and all the other logic described above). -However, prepaid credits and usage limits are becoming the default standard for AI monetization. This needs to work in real-time. +Autumn flips this. The `check` function is a low-latency API designed to be called _before_ an action is taken, to gate access based on the customer's plan and balance. Because it runs before the action, Autumn becomes your system of record for subscriptions and entitlements — not your database or Stripe. -Put simply, Autumn's key differentiator is the `check` function: a low-latency API called designed to be called _before_ an action is taken, to gate access. This sounds trivial, but is a very different product in 2 ways: +This means credit ledgers, real-time enforcement, spend caps, and edge cases around upgrades, downgrades and failed payments are handled automatically. Changing pricing, migrating plans, or setting up custom enterprise contracts become configuration changes instead of code changes. -**Functionally**
-Because check runs before the action, Autumn becomes your system of record for pricing and entitlements. +Architecture-wise, `check` is designed to be called inline — before every AI generation, API request, or feature gate. Response times are under 50ms via multi-region caching, with atomic handling of concurrent requests so balances stay consistent at scale. -With other providers, billing is asynchronous: you send usage, they generate invoices later, and _you own the logic and state in between_ — who gets access, when limits apply, how downgrades work etc. - -Autumn owns that layer. Credit ledgers, real-time enforcement and spend caps work out of the box. Edge cases around upgrades, downgrades, and failed payments are handled automatically — users always get access to what they've paid for. - -That also makes scaling challenges much simpler. Changing pricing, migrating plans, handling downgrades, setting up custom enterprise contracts with rollovers, or launching team billing with per-seat allowances become configuration changes instead of messy logic rewrites. - - -**Architecturally**
-`check` is designed to be called inline, before every AI generation, API request, or feature gate. We achieve response times of under 50ms via multi-region caching, with atomic handling of concurrent requests so credit balances and usage limits stay consistent even at scale. - -Unlike other billing providers, Autumn is built on top of Stripe billing instead of replacing it. Your subscriptions, customers, and payment details live in your own Stripe account, so you're never "locked in". +Autumn is built on top of Stripe rather than replacing it. Your subscriptions, customers, and payment details live in your own Stripe account. -While Autumn's core focus is credit-based AI monetization, it can be used for any SaaS pricing model. Many of our users have no usage-based features at all, and just prefer the simplicity and developer experience (eg, no webhooks). +While Autumn's core focus is credit-based AI monetization, it handles any SaaS pricing model. Many of our users have no usage-based features at all, and just prefer the developer experience (eg, no webhooks). @@ -98,8 +87,7 @@ While Autumn's core focus is credit-based AI monetization, it can be used for an - -## Core concepts +## Core flow diff --git a/bun.lock b/bun.lock index 32dd1e739..0267d6f97 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "autumn", @@ -446,6 +445,7 @@ "ag-grid-community": "^34.0.2", "ag-grid-react": "^34.0.2", "ai": "^6.0.5", + "atmn": "workspace:*", "autumn-js": "workspace:*", "axios": "^1.8.3", "better-auth": "catalog:", diff --git a/packages/atmn/package.json b/packages/atmn/package.json index c6d2d79f1..6072952a2 100644 --- a/packages/atmn/package.json +++ b/packages/atmn/package.json @@ -12,6 +12,18 @@ "homepage": "https://docs.useautumn.com/api-reference/cli/getting-started", "main": "dist/compose/index.js", "types": "dist/src/compose/index.d.ts", + "exports": { + ".": { + "types": "./dist/src/compose/index.d.ts", + "import": "./dist/compose/index.js", + "default": "./dist/compose/index.js" + }, + "./skills": { + "types": "./src/prompts/skills/index.ts", + "import": "./src/prompts/skills/index.ts", + "default": "./src/prompts/skills/index.ts" + } + }, "type": "module", "engines": { "node": ">=16" diff --git a/packages/atmn/src/lib/hooks/useCreateGuides.ts b/packages/atmn/src/lib/hooks/useCreateGuides.ts index 1f171d7c0..a4312573f 100644 --- a/packages/atmn/src/lib/hooks/useCreateGuides.ts +++ b/packages/atmn/src/lib/hooks/useCreateGuides.ts @@ -1,10 +1,12 @@ import fs from "node:fs/promises"; import path from "node:path"; import { useState } from "react"; -import { customerPrompt } from "../../prompts/customer.js"; -import { paymentsPrompt } from "../../prompts/payments.js"; -import { pricingPrompt } from "../../prompts/pricing.js"; -import { usagePrompt } from "../../prompts/usage.js"; +import { + autumnBillingPageContent, + autumnGatingContent, + autumnModellingPricingPlansContent, + autumnSetupContent, +} from "../../prompts/skills/index.js"; const GUIDES_DIR = "autumn-guides"; @@ -27,33 +29,31 @@ export function useCreateGuides() { const created: string[] = []; - // Always write customer, payments, usage guides await fs.writeFile( - path.join(guidesPath, "1_Customer_Creation.md"), - customerPrompt, + path.join(guidesPath, "1_Setup.md"), + autumnSetupContent, "utf-8", ); - created.push("1_Customer_Creation.md"); + created.push("1_Setup.md"); await fs.writeFile( - path.join(guidesPath, "2_Accepting_Payments.md"), - paymentsPrompt, + path.join(guidesPath, "2_Gating.md"), + autumnGatingContent, "utf-8", ); - created.push("2_Accepting_Payments.md"); + created.push("2_Gating.md"); await fs.writeFile( - path.join(guidesPath, "3_Tracking_Usage.md"), - usagePrompt, + path.join(guidesPath, "3_Billing_Page.md"), + autumnBillingPageContent, "utf-8", ); - created.push("3_Tracking_Usage.md"); + created.push("3_Billing_Page.md"); - // Only write pricing guide if user doesn't have pricing yet (or saveAll is true) if (options?.saveAll || !hasPricing) { await fs.writeFile( path.join(guidesPath, "0_Designing_Pricing.md"), - pricingPrompt, + autumnModellingPricingPlansContent, "utf-8", ); created.unshift("0_Designing_Pricing.md"); diff --git a/packages/atmn/src/lib/hooks/useCreateSkills.ts b/packages/atmn/src/lib/hooks/useCreateSkills.ts index 02dbac26e..5241c625c 100644 --- a/packages/atmn/src/lib/hooks/useCreateSkills.ts +++ b/packages/atmn/src/lib/hooks/useCreateSkills.ts @@ -1,14 +1,17 @@ import fs from "node:fs/promises"; import path from "node:path"; import { useState } from "react"; -import { skills, type Skill } from "../../prompts/skills/index.js"; +import { type Skill, skills } from "../../prompts/skills/index.js"; type CreateSkillsState = "idle" | "creating" | "done" | "error"; export type SkillsLocation = ".claude/skills" | ".agents/skills" | "custom"; export interface UseCreateSkillsResult { - create: (targetDir: string, options?: { saveAll?: boolean; hasPricing?: boolean }) => Promise; + create: ( + targetDir: string, + options?: { saveAll?: boolean; hasPricing?: boolean }, + ) => Promise; state: CreateSkillsState; filesCreated: string[]; error: string | null; @@ -18,12 +21,12 @@ export interface UseCreateSkillsResult { /** * Hook to create AI skill files in the SKILLS standard format. * Skills are saved as SKILL.md files in subdirectories: - * + * * / - * autumn-customer/SKILL.md - * autumn-payments/SKILL.md - * autumn-pricing/SKILL.md - * autumn-usage/SKILL.md + * autumn-setup/SKILL.md + * autumn-gating/SKILL.md + * autumn-billing-page/SKILL.md + * autumn-modelling-pricing-plans/SKILL.md */ export function useCreateSkills(): UseCreateSkillsResult { const [state, setState] = useState("idle"); @@ -41,13 +44,17 @@ export function useCreateSkills(): UseCreateSkillsResult { try { const cwd = process.cwd(); const skillsPath = path.join(cwd, targetDir); - + const created: string[] = []; // Filter skills based on options const skillsToCreate = skills.filter((skill) => { // Skip pricing skill if user already has pricing (unless saveAll is true) - if (skill.id === "autumn-pricing" && !options?.saveAll && options?.hasPricing) { + if ( + skill.id === "autumn-pricing" && + !options?.saveAll && + options?.hasPricing + ) { return false; } return true; @@ -57,10 +64,10 @@ export function useCreateSkills(): UseCreateSkillsResult { for (const skill of skillsToCreate) { const skillDir = path.join(skillsPath, skill.id); await fs.mkdir(skillDir, { recursive: true }); - + const skillFilePath = path.join(skillDir, "SKILL.md"); await fs.writeFile(skillFilePath, skill.content, "utf-8"); - + created.push(`${skill.id}/SKILL.md`); } diff --git a/packages/atmn/src/prompts/creditSystemDocs.ts b/packages/atmn/src/prompts/creditSystemDocs.ts deleted file mode 100644 index ba6a24aa4..000000000 --- a/packages/atmn/src/prompts/creditSystemDocs.ts +++ /dev/null @@ -1,662 +0,0 @@ -export const creditSystemDocs = ` -# Monetary credits - -> Grant your users a currency-based balance of credits, that various features can draw from - -When you have multiple features that cost different amounts, you can use a credit system to deduct usage from a single balance. This can be great to simplify billing and usage tracking, especially when you have lots of features. - -## Example case - -We have a AI chatbot product with 2 different models, and each model costs a different amount to use. - -* Basic message: $1 per 100 messages -* Premium message: $10 per 100 messages - -And we have the following plans: - -* Free tier: $5 credits per month for free -* Pro tier: $10 credits per month, at $10 per month - -Users should also be able to top up their balance with more credits. - -## Configure Pricing - - - - #### Create Features - - Create a \`metered\` \`consumable\` feature for each message type, so that we can track the usage of each: - - - - - - - - - - #### Create Credit System - - Now, we'll create a credit system, where we'll define the cost of each message type. We'll define the cost per message in USD: - - | Feature | Cost per message (USD) | Credit cost per message (USD) | - | --------------- | ---------------------- | ----------------------------- | - | Basic message | $1 per 100 messages | 0.01 | - | Premium message | $10 per 100 messages | 0.10 | - - - - - - - - - - #### Create Free, Pro and Top-up Plans - - Let's create our free and pro plans, and add the credits amounts to each. - - - Make sure to set the \`auto-enable\` flag on the free plan, so that it is automatically assigned to new customers. - - - - - - - - -
- - - - - - - - Then, we'll create our top-up plan. We'll add a price to our credit feature, where each credit is worth $1. These top up credits will be \`one-off\` \`prepaid\` purchases that never expire. - - - - - - -
-
- -## Implementation - - - - #### Create an Autumn Customer - - When your user signs up, create an Autumn customer. This will automatically assign them the Free plan, and grant them $5 credits per month. - - - \`\`\`jsx React theme={null} - import { useCustomer } from "autumn-js/react"; - - const App = () => { - const { customer } = useCustomer(); - - console.log("Autumn customer:", customer); - - return

Welcome, {customer?.name || "user"}!

; - }; - \`\`\` - - \`\`\`typescript Node.js theme={null} - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - const { data, error } = await autumn.customers.create({ - id: "user_or_org_id_from_auth", - name: "John Yeo", - email: "john@example.com", - }); - \`\`\` - - \`\`\`python Python theme={null} - import asyncio - from autumn import Autumn - - autumn = Autumn('am_sk_42424242') - - async def main(): - customer = await autumn.customers.create( - id="user_or_org_id_from_auth", - name="John Yeo", - email="john@example.com", - ) - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL theme={null} - curl --request POST \ - --url https://api.useautumn.com/customers \ - --header 'Authorization: Bearer am_sk_42424242' \ - --header 'Content-Type: application/json' \ - --data '{ - "id": "user_or_org_id_from_auth", - "name": "John Yeo", - "email": "john@example.com" - }' - \`\`\` -
-
- - - #### Checking for access - - Every time our user sends a message to the chatbot, we'll first check if they have enough credits remaining to send the message. - - The \`required_balance\` parameter will convert the number of messages to credits. Eg, if you pass \`required_balance: 5\` for basic messages, then check will return \`allowed: true\` if the user has at least 0.05 USD credits remaining. - - - Note how we're interacting with the underlying features (\`basic_messages\`, - \`premium_messages\`) here--not the credit system. - - - - \`\`\`jsx React wrap theme={null} - import { useCustomer } from "autumn-js/react"; - - export function CheckBasicMessage() { - const { check, refetch } = useCustomer(); - - const handleCheckAccess = async () => { - const { data } = await check({ featureId: "basic_messages", requiredBalance: 1 }); - - if (!data?.allowed) { - alert("You've run out of basic message credits"); - } else { - // proceed with sending message - await refetch(); - } - }; - } - \`\`\` - - \`\`\`typescript Node.js theme={null} - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - const { data } = await autumn.check({ - customer_id: "user_or_org_id_from_auth", - feature_id: "basic_messages", - required_balance: 1, - }); - - if (!data.allowed) { - console.log("User has run out of basic message credits"); - return; - } - \`\`\` - - \`\`\`python Python theme={null} - import asyncio - from autumn import Autumn - - autumn = Autumn("am_sk_42424242") - - async def main(): - response = await autumn.check( - customer_id="user_or_org_id_from_auth", - feature_id="basic_messages", - required_balance=1, - ) - - if not response.allowed: - print("User has run out of basic message credits") - return - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL theme={null} - curl -X POST "https://api.useautumn.com/v1/check" \ - -H "Authorization: Bearer am_sk_42424242" \ - -H "Content-Type: application/json" \ - -d '{ - "customer_id": "user_or_org_id_from_auth", - "feature_id": "basic_messages", - "required_balance": 1 - }' - \`\`\` - - - - The credit system ID will be returned in the \`balances\` field. - - \`\`\`json {8} theme={null} - { - "allowed": true, - "code": "feature_found", - "customer_id": "ayush", - "feature_id": "usd_credits", - "required_balance": 0.01, - "interval": "month", - "interval_count": 1, - "unlimited": false, - "balance": 5, - "usage": 0, - "included_usage": 5, - "next_reset_at": 1769110978704, - "overage_allowed": false, - "credit_schema": [ - { - "feature_id": "basic_messages", - "credit_amount": 0.01 - }, - { - "feature_id": "premium_messages", - "credit_amount": 0.1 - } - ] - } - \`\`\` - - - - - #### Tracking messages and using credits - - Now let's implement our usage tracking and use up our credits. In this example, we're using 2 basic messages, which will cost us 0.02 USD credits. - - - \`\`\`typescript Node.js theme={null} - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - await autumn.track({ - customer_id: "user_or_org_id_from_auth", - feature_id: "basic_messages", - value: 2, - }); - \`\`\` - - \`\`\`python Python theme={null} - import asyncio - from autumn import Autumn - - autumn = Autumn("am_sk_42424242") - - async def main(): - await autumn.track( - customer_id="user_or_org_id_from_auth", - feature_id="basic_messages", - value=2, - ) - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL theme={null} - curl -X POST "https://api.useautumn.com/v1/track" \ - -H "Authorization: Bearer am_sk_42424242" \ - -H "Content-Type: application/json" \ - -d '{ - "customer_id": "user_or_org_id_from_auth", - "feature_id": "basic_messages", - "value": 2 - }' - \`\`\` - - - - \`\`\`json theme={null} - { - "code": "event_received", - "customer_id": "user_or_org_id_from_auth", - "feature_id": "basic_messages" - } - \`\`\` - - - - - #### Upgrading to Pro - - We can prompt the user to upgrade. When they click our "upgrade" button, we can use the \`checkout\` route to get a Stripe Checkout URL for them to make a payment. - - - \`\`\`jsx React theme={null} - import { useCustomer, CheckoutDialog } from "autumn-js/react"; - - export default function UpgradeButton() { - const { checkout } = useCustomer(); - - return ( - - ); - } - \`\`\` - - \`\`\`typescript Node.js theme={null} - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - const { data } = await autumn.checkout({ - customer_id: "user_or_org_id_from_auth", - product_id: "pro", - }); - - if (data.url) { - // Redirect user to Stripe checkout URL - } else { - // Show upgrade preview to user - } - \`\`\` - - \`\`\`python Python theme={null} - import asyncio - from autumn import Autumn - - autumn = Autumn("am_sk_42424242") - - async def main(): - response = await autumn.checkout( - customer_id="user_or_org_id_from_auth", - product_id="pro" - ) - - if response.url: - # Redirect user to Stripe checkout URL - pass - else: - # Show upgrade preview to user - pass - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL theme={null} - curl -X POST "https://api.useautumn.com/v1/checkout" \ - -H "Authorization: Bearer am_sk_42424242" \ - -H "Content-Type: application/json" \ - -d '{ - "customer_id": "user_or_org_id_from_auth", - "product_id": "pro" - }' - \`\`\` - - - - \`\`\`json theme={null} - { - "customer_id": "user_or_org_id_from_auth", - "lines": [ - { - "description": "Pro - $10 / month", - "amount": 10, - "item": { - "type": "price", - "feature_id": null, - "feature": null, - "interval": "month", - "interval_count": 1, - "price": 10, - "display": { - "primary_text": "$10", - "secondary_text": "per month" - } - } - } - ], - "product": { - "id": "pro", - "name": "Pro", - "group": null, - "env": "sandbox", - "is_add_on": false, - "is_default": false, - "archived": false, - "version": 1, - "created_at": 1766428038264, - "items": [ - { - "type": "price", - "feature_id": null, - "feature": null, - "interval": "month", - "interval_count": 1, - "price": 10, - "display": { - "primary_text": "$10", - "secondary_text": "per month" - } - }, - { - "type": "feature", - "feature_id": "usd_credits", - "feature_type": "single_use", - "feature": { - "id": "usd_credits", - "name": "USD credits", - "type": "credit_system", - "display": { - "singular": "USD credits", - "plural": "USD credits" - }, - "credit_schema": [ - { - "metered_feature_id": "basic_messages", - "credit_cost": 0.01 - }, - { - "metered_feature_id": "premium_messages", - "credit_cost": 0.1 - } - ] - }, - "included_usage": 10, - "interval": "month", - "interval_count": 1, - "reset_usage_when_enabled": true, - "entity_feature_id": null, - "display": { - "primary_text": "10 USD credits" - } - } - ], - "free_trial": null, - "base_variant_id": null, - "scenario": "upgrade", - "properties": { - "is_free": false, - "is_one_off": false, - "interval_group": "month", - "has_trial": false, - "updateable": false - } - }, - "current_product": { - "id": "free", - "name": "Free", - "group": null, - "env": "sandbox", - "is_add_on": false, - "is_default": true, - "archived": false, - "version": 1, - "created_at": 1766427877578, - "items": [ - { - "type": "feature", - "feature_id": "usd_credits", - "feature_type": "single_use", - "feature": { - "id": "usd_credits", - "name": "USD credits", - "type": "credit_system", - "display": { - "singular": "USD credits", - "plural": "USD credits" - }, - "credit_schema": [ - { - "metered_feature_id": "basic_messages", - "credit_cost": 0.01 - }, - { - "metered_feature_id": "premium_messages", - "credit_cost": 0.1 - } - ] - }, - "included_usage": 5, - "interval": "month", - "interval_count": 1, - "reset_usage_when_enabled": true, - "entity_feature_id": null, - "display": { - "primary_text": "5 USD credits", - "secondary_text": "per month" - } - } - ], - "free_trial": null, - "base_variant_id": null, - "scenario": "new", - "properties": { - "is_free": true, - "is_one_off": false, - "has_trial": false, - "updateable": false - } - }, - "options": [], - "total": 10, - "currency": "usd", - "url": "https://checkout.stripe.com/c/pay/.......", - "has_prorations": false - } - \`\`\` - - - - - #### Purchasing Top-ups - - When users run low on credits, they can purchase additional credits using our top-up plan. In this example, the user is purchasing 20 USD credits, which will cost them $20. - - - \`\`\`jsx React theme={null} - import { useCustomer, CheckoutDialog } from "autumn-js/react"; - - export default function TopUpButton() { - const { checkout } = useCustomer(); - - return ( - - ); - } - \`\`\` - - \`\`\`typescript Node.js theme={null} - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - const { data } = await autumn.attach({ - customer_id: "user_or_org_id_from_auth", - product_id: "top_up", - options: [{ - feature_id: "usd_credits", - quantity: 20, - }], - }); - \`\`\` - - \`\`\`python Python theme={null} - import asyncio - from autumn import Autumn - - autumn = Autumn("am_sk_42424242") - - async def main(): - response = await autumn.attach( - customer_id="user_or_org_id_from_auth", - product_id="top_up", - options=[{ - "feature_id": "usd_credits", - "quantity": 20, - }], - ) - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL theme={null} - curl -X POST "https://api.useautumn.com/v1/attach" \ - -H "Authorization: Bearer am_sk_42424242" \ - -H "Content-Type: application/json" \ - -d '{ - "customer_id": "user_or_org_id_from_auth", - "product_id": "top_up", - "options": [{ - "feature_id": "usd_credits", - "quantity": 20, - }] - }' - \`\`\` - - - - \`\`\`json theme={null} - { - "success": true, - "customer_id": "user_or_org_id_from_auth", - "product_ids": [ - "top_up" - ], - "code": "one_off_product_attached", - "message": "Successfully purchased product(s) Top up and attached to customer John" - } - \`\`\` - - -
- - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.useautumn.com/llms.txt -`; diff --git a/packages/atmn/src/prompts/customer.ts b/packages/atmn/src/prompts/customer.ts deleted file mode 100644 index 6591a2574..000000000 --- a/packages/atmn/src/prompts/customer.ts +++ /dev/null @@ -1,296 +0,0 @@ -export const customerPrompt = `## Set up Autumn billing integration - -Autumn is a billing and entitlements layer over Stripe, which we are adding into this codebase. Your task is to create an Autumn customer by following these steps, and add it to a place in this app where it will be automatically created. - -### Step 1: Analyze my codebase - -Before making changes, detect: -- Language (TypeScript/JavaScript, Python, or other) -- If TS/JS: Framework (Next.js, React Router, Tanstack Start, Hono, Express, Fastify, or other) -- If TS/JS: Is there a React frontend? (Check for React in package.json) - -Also ask me: - -**1. Should Autumn customers be individual users, or organizations?** -- Users (B2C): Each user has their own plan and limits -- Organizations (B2B): Plans and limits are shared across an org - -**2. Have you created an AUTUMN_SECRET_KEY and added it to .env?** -Please prompt them to create one here: https://app.useautumn.com/dev?tab=api_keys and add it to .env as AUTUMN_SECRET_KEY - - - -Tell me what you detected, which path you'll follow and what you'll be adding autumn to. - ---- - -## Path A: React + Node.js (fullstack TypeScript) - -Use this path if there's a React frontend with a Node.js backend. - -### A1. Install the SDK - -**Use the package manager already installed** -- eg user may be using bun, or pnpm. -\`\`\`bash -npm install autumn-js -\`\`\` - -### A2. Mount the handler (server-side) - -This creates endpoints at \`/api/autumn/*\` that the React hooks will call. The \`identify\` function should return either the user ID or org ID from your auth provider, depending on how you're using Autumn. - -**Next.js (App Router):** -\`\`\`typescript -// app/api/autumn/[...all]/route.ts -import { autumnHandler } from "autumn-js/next"; - -export const { GET, POST } = autumnHandler({ - identify: async (request) => { - // Get user/org from your auth provider - const session = await auth.api.getSession({ headers: request.headers }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { - name: session?.user.name, - email: session?.user.email, - }, - }; - }, -}); -\`\`\` - -**React Router:** -\`\`\`typescript -// app/routes/api.autumn.tsx -import { autumnHandler } from "autumn-js/react-router"; - -export const { loader, action } = autumnHandler({ - identify: async (args) => { - const session = await auth.api.getSession({ headers: args.request.headers }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, -}); - -// routes.ts - add this route -route("api/autumn/*", "routes/api.autumn.tsx") -\`\`\` - -**Tanstack Start:** -\`\`\`typescript -// routes/api/autumn.$.ts -import { autumnHandler } from "autumn-js/tanstack"; - -const handler = autumnHandler({ - identify: async ({ request }) => { - const session = await auth.api.getSession({ headers: request.headers }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, -}); - -export const Route = createFileRoute("/api/autumn/$")({ - server: { handlers: handler }, -}); -\`\`\` - -**Hono:** -\`\`\`typescript -import { autumnHandler } from "autumn-js/hono"; - -app.use("/api/autumn/*", autumnHandler({ - identify: async (c) => { - const session = await auth.api.getSession({ headers: c.req.raw.headers }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, -})); -\`\`\` - -**Express:** -\`\`\`typescript -import { autumnHandler } from "autumn-js/express"; - -app.use(express.json()); // Must be before autumnHandler -app.use("/api/autumn", autumnHandler({ - identify: async (req) => { - const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, -})); -\`\`\` - -**Fastify:** -\`\`\`typescript -import { autumnHandler } from "autumn-js/fastify"; - -fastify.route({ - method: ["GET", "POST"], - url: "/api/autumn/*", - handler: autumnHandler({ - identify: async (request) => { - const session = await auth.api.getSession({ headers: request.headers as any }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, - }), -}); -\`\`\` - -**Other frameworks (generic handler):** -\`\`\`typescript -import { autumnHandler } from "autumn-js/backend"; - -// Mount this handler onto the /api/autumn/* path in your backend -const handleRequest = async (request) => { - // Your authentication logic here - const customerId = "user_or_org_id_from_auth"; - - let body = null; - if (request.method !== "GET") { - body = await request.json(); - } - - const { statusCode, response } = await autumnHandler({ - customerId, - customerData: { name: "", email: "" }, - request: { - url: request.url, - method: request.method, - body: body, - }, - }); - - return new Response(JSON.stringify(response), { - status: statusCode, - headers: { "Content-Type": "application/json" }, - }); -}; -\`\`\` - -### A3. Add the provider (client-side) - -Wrap your app with AutumnProvider: -\`\`\`tsx -import { AutumnProvider } from "autumn-js/react"; - -export default function RootLayout({ children }) { - return ( - - {children} - - ); -} -\`\`\` - -If your backend is on a different URL (e.g., Vite + separate server), pass \`backendUrl\`: -\`\`\`tsx - -\`\`\` - -### A4. Create a test customer - -Add this hook to any component to verify the integration: -\`\`\`tsx -import { useCustomer } from "autumn-js/react"; - -const { customer } = useCustomer(); -console.log("Autumn customer:", customer); -\`\`\` - -This automatically creates an Autumn customer for new users/orgs. - ---- - -## Path B: Backend only (Node.js, Python, or other) - -Use this path if there's no React frontend, or you prefer server-side only. - -### B1. Install the SDK -\`\`\`bash -# Node.js -npm install autumn-js - -# Python -pip install autumn-py -\`\`\` - -### B2. Initialize the client - -**TypeScript/JavaScript:** -\`\`\`typescript -import { Autumn } from "autumn-js"; - -const autumn = new Autumn({ - secretKey: process.env.AUTUMN_SECRET_KEY, -}); -\`\`\` - -**Python:** -\`\`\`python -from autumn import Autumn - -autumn = Autumn('am_sk_test_xxx') -\`\`\` - -### B3. Create a test customer - -This will GET or CREATE a new customer. Add it when a user signs in or loads the app. Pass in ID from auth provider. -The response returns customer state, used to display billing information client-side. Please console.log the Autumn customer client-side. - -**TypeScript:** -\`\`\`typescript -const { data, error } = await autumn.customers.create({ - id: "user_or_org_id_from_auth", - name: "Test User", - email: "test@example.com", -}); -\`\`\` - -**Python:** -\`\`\`python -customer = await autumn.customers.create( - id="user_or_org_id_from_auth", - name="Test User", - email="test@example.com", -) -\`\`\` - -**cURL:** -\`\`\`bash -curl -X POST https://api.useautumn.com/customers \\ - -H "Authorization: Bearer am_sk_test_xxx" \\ - -H "Content-Type: application/json" \\ - -d '{"id": "user_or_org_id_from_auth", "name": "Test User", "email": "test@example.com"}' -\`\`\` - -When calling these functions from the client, the SDK exports types for all response objects. Use these for type-safe code. - -\`\`\`tsx -import type { Customer } from "autumn-js"; -\`\`\` - ---- - -## Verify - -After setup, tell me: -1. What stack you detected -2. Which path you followed -3. What files you created/modified -4. That the Autumn customer is logged in browser, and to check in the Autumn dashboard - -**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root. - -Docs: https://docs.useautumn.com/llms.txt`; diff --git a/packages/atmn/src/prompts/payments.ts b/packages/atmn/src/prompts/payments.ts deleted file mode 100644 index 31300670d..000000000 --- a/packages/atmn/src/prompts/payments.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { prepaidDocs } from "./prepaidDocs.js"; - -export const paymentsPrompt = `## Add Autumn payment flow - -Autumn handles Stripe checkout and plan changes. Your task is to add the payment flow to this codebase for ALL plans in the Autumn configuration. - -### Step 1: Detect my integration type - -Check if this codebase already has Autumn set up: -- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted → **Path A: React** -- If there's just an \`Autumn\` client initialized → **Path B: Backend SDK** - -Before implementing: -1. Tell me which path you'll follow before proceeding. -2. Tell me that I will be building pricing cards to handle billing flows, and ask for any guidance or any input - ---- - -## Path A: React - -### Checkout Flow - -Use \`checkout\` from \`useCustomer\`. It returns either a Stripe URL (new customer) or checkout preview data (returning customer with card on file). - -\`\`\`tsx -import { useCustomer } from "autumn-js/react"; - -const { checkout } = useCustomer(); - -const data = await checkout({ productId: "pro" }); - -if (!data.url) { - // Returning customer → show confirmation dialog with result data - // data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle } -} -\`\`\` - -After user confirms in your dialog, call \`attach\` to enable plan (and charge card as needed) - -\`\`\`tsx -const { attach } = useCustomer(); - -await attach({ productId: "pro" }); -\`\`\` - -### Getting Billing State - -Use \`usePricingTable\` to get products with their billing scenario and display state. - -\`\`\`tsx -import { usePricingTable } from "autumn-js/react"; - -function PricingPage() { - const { products } = usePricingTable(); - // Each product has: scenario, properties - // scenario: "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel" -} -\`\`\` - -### Canceling -Only use this if there is no free plan in the user's Autumn config. If there is a free plan, then you can cancel by attaching the free plan. - -\`\`\`tsx -const { cancel } = useCustomer(); -await cancel({ productId: "pro" }); -\`\`\` - ---- - -## Path B: Backend SDK - -### Checkout Flow - -Payments are a 2-step process: -1. **checkout** - Returns Stripe checkout URL (new customer) or preview data (returning customer) -2. **attach** - Confirms purchase when no URL was returned - -**TypeScript:** -\`\`\`typescript -import { Autumn } from "autumn-js"; -import type { CheckoutResult, AttachResult } from "autumn-js"; - -const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY }); - -// Step 1: Get checkout info -const { data } = await autumn.checkout({ - customer_id: "user_or_org_id_from_auth", - product_id: "pro", -}) as { data: CheckoutResult }; - -if (data.url) { - // New customer → redirect to Stripe - return redirect(data.url); -} else { - // Returning customer → return preview data for confirmation UI - // data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle } - return data; -} - -// Step 2: After user confirms (only if no URL) -const { data: attachData } = await autumn.attach({ - customer_id: "user_or_org_id_from_auth", - product_id: "pro", -}) as { data: AttachResult }; -\`\`\` - -**Python:** -\`\`\`python -from autumn import Autumn - -autumn = Autumn('am_sk_test_xxx') - -# Step 1: Get checkout info -response = await autumn.checkout( - customer_id="user_or_org_id_from_auth", - product_id="pro", -) - -if response.url: - # New customer → redirect to Stripe - return redirect(response.url) -else: - # Returning customer → return preview data for confirmation UI - return response - -# Step 2: After user confirms -attach_response = await autumn.attach( - customer_id="user_or_org_id_from_auth", - product_id="pro", -) -\`\`\` - -For prepaid pricing options, see the end of this message. - -### Getting Billing State - -Use \`products.list\` with a \`customer_id\` to get products with their billing scenario. **Don't build custom billing state logic.** - -**TypeScript:** -\`\`\`typescript -const { data } = await autumn.products.list({ - customer_id: "user_or_org_id_from_auth", -}); - -data.list.forEach((product) => { - const { scenario } = product; - // "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel" -}); -\`\`\` - -**Python:** -\`\`\`python -response = await autumn.products.list(customer_id="user_or_org_id_from_auth") - -for product in response.list: - scenario = product.scenario -\`\`\` - -**curl:** -\`\`\`bash -curl https://api.useautumn.com/v1/products?customer_id=user_or_org_id_from_auth \\ - -H "Authorization: Bearer $AUTUMN_SECRET_KEY" -\`\`\` - -### Canceling - -\`\`\`typescript -await autumn.cancel({ customer_id: "...", product_id: "pro" }); -\`\`\` - -Or attach a free product ID to downgrade. - ---- - -## Common Patterns - -### Pricing Button Text - -\`\`\`typescript -const SCENARIO_TEXT: Record = { - scheduled: "Plan Scheduled", - active: "Current Plan", - renew: "Renew", - upgrade: "Upgrade", - new: "Enable", - downgrade: "Downgrade", - cancel: "Cancel Plan", -}; - -export const getPricingButtonText = (product: Product): string => { - const { scenario, properties } = product; - const { is_one_off, updateable, has_trial } = properties ?? {}; - - if (has_trial) return "Start Trial"; - if (scenario === "active" && updateable) return "Update"; - if (scenario === "new" && is_one_off) return "Purchase"; - - return SCENARIO_TEXT[scenario ?? ""] ?? "Enable Plan"; -}; -\`\`\` - -### Confirmation Dialog Text - -\`\`\`typescript -import type { CheckoutResult, Product } from "autumn-js"; - -export const getConfirmationTexts = (result: CheckoutResult): { title: string; message: string } => { - const { product, current_product, next_cycle } = result; - const scenario = product.scenario; - const productName = product.name; - const currentProductName = current_product?.name; - const nextCycleDate = next_cycle?.starts_at - ? new Date(next_cycle.starts_at).toLocaleDateString() - : undefined; - - const isRecurring = !product.properties?.is_one_off; - - const CONFIRMATION_TEXT: Record = { - scheduled: { title: "Already Scheduled", message: "You already have this product scheduled." }, - active: { title: "Already Active", message: "You are already subscribed to this product." }, - renew: { title: "Renew", message: \`Renew your subscription to \${productName}.\` }, - upgrade: { title: \`Upgrade to \${productName}\`, message: \`Upgrade to \${productName}. Your card will be charged immediately.\` }, - downgrade: { title: \`Downgrade to \${productName}\`, message: \`\${currentProductName} will be cancelled. \${productName} begins \${nextCycleDate}.\` }, - cancel: { title: "Cancel", message: \`Your subscription to \${currentProductName} will end \${nextCycleDate}.\` }, - }; - - if (scenario === "new") { - return isRecurring - ? { title: \`Subscribe to \${productName}\`, message: \`Subscribe to \${productName}. Charged immediately.\` } - : { title: \`Purchase \${productName}\`, message: \`Purchase \${productName}. Charged immediately.\` }; - } - - return CONFIRMATION_TEXT[scenario ?? ""] ?? { title: "Change Subscription", message: "You are about to change your subscription." }; -}; -\`\`\` - ---- - -## Notes - -- **NB: the result is \`data.url\`, NOT \`data.checkout_url\`** -- This handles all upgrades, downgrades, renewals, uncancellations automatically -- Product IDs come from the Autumn configuration -- Pass \`successUrl\` to \`checkout\` to redirect users after payment -- For prepaid pricing examples, see the end of this message. - -**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root. - -Docs: https://docs.useautumn.com/llms.txt - - -${prepaidDocs} - -`; diff --git a/packages/atmn/src/prompts/prepaidDocs.ts b/packages/atmn/src/prompts/prepaidDocs.ts deleted file mode 100644 index 45c8f6d1b..000000000 --- a/packages/atmn/src/prompts/prepaidDocs.ts +++ /dev/null @@ -1,463 +0,0 @@ -export const prepaidDocs = ` - -# Prepaid top-ups - -> Let customers purchase prepaid packages and top-ups. - -If a user hits a usage limit you granted them, they may be willing to purchase a top-up. - -These are typically one-time purchases (or less commonly, recurring add-ons) that grant a fixed usage of a feature. - -This gives users full spend control and allows your business to be paid upfront. For these reasons, it tends to be a more popular alternative to usage-based pricing -- eg, OpenAI uses this model for their API. - -## Example case - -In this example, we have an AI chatbot that offers: - -* 10 premium messages for free -* An option for customers to top-up premium messages in packages of $10 per 100 messages. - -## Configure Pricing - - - - #### Create Features - - Create a \`metered\` \`consumable\` feature for our premium messages, so we can track its balance. - - - - - - - - - - #### Create Free and Top-up Plans - - Create our free plan, and assign 10 premium messages to it. These are "one-off" credits, that will not reset periodically. - - - Make sure to set the \`auto-enable\` flag on the free plan, so that it is automatically assigned to new customers. - - - - - - - - - Now we'll create our top-up plan. We'll add a price to our premium messages feature, at $10 per 100 messages. These are "one-off" purchases, with a \`prepaid\` billing method. - - \`prepaid\` features require a \`quantity\` to be sent in when a customer attaches this product, so the customer can specify how many premium messages they want to top up with. - - - - - - - - - -## Implementation - - - - #### Create an Autumn Customer - - When your user signs up, create an Autumn customer. This will automatically assign them the Free plan, and grant them 10 premium messages. - - - \`\`\`jsx React - import { useCustomer } from "autumn-js/react"; - - const App = () => { - const { customer } = useCustomer(); - - console.log("Autumn customer:", customer); - - return

Welcome, {customer?.name || "user"}!

; - }; - \`\`\` - - \`\`\`typescript Node.js - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - const { data, error } = await autumn.customers.create({ - id: "user_or_org_id_from_auth", - name: "John Yeo", - email: "john@example.com", - }); - \`\`\` - - \`\`\`python Python - import asyncio - from autumn import Autumn - - autumn = Autumn('am_sk_42424242') - - async def main(): - customer = await autumn.customers.create( - id="user_or_org_id_from_auth", - name="John Yeo", - email="john@example.com", - ) - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL - curl --request POST \ - --url https://api.useautumn.com/customers \ - --header 'Authorization: Bearer am_sk_42424242' \ - --header 'Content-Type: application/json' \ - --data '{ - "id": "user_or_org_id_from_auth", - "name": "John Yeo", - "email": "john@example.com" - }' - \`\`\` -
-
- - - #### Checking for access - - Every time our user wants to send a premium message, we'll first check if they have enough premium messages remaining. - - - \`\`\`jsx React wrap - import { useCustomer } from "autumn-js/react"; - - export function CheckPremiumMessage() { - const { check, refetch } = useCustomer(); - - const handleCheckAccess = async () => { - const { data } = await check({ featureId: "premium-messages" }); - - if (!data?.allowed) { - alert("You've run out of premium messages"); - } else { - // proceed with sending message - await refetch(); - } - }; - } - \`\`\` - - \`\`\`typescript Node.js theme={null} - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - const { data } = await autumn.check({ - customer_id: "user_or_org_id_from_auth", - feature_id: "premium_messages", - }); - - if (!data.allowed) { - console.log("User has run out of premium messages"); - return; - } - \`\`\` - - \`\`\`python Python theme={null} - import asyncio - from autumn import Autumn - - autumn = Autumn("am_sk_1234567890") - - async def main(): - response = await autumn.check( - customer_id="user_or_org_id_from_auth", - feature_id="premium_messages", - ) - - if not response.allowed: - print("User has run out of premium messages") - return - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL theme={null} - curl -X POST "https://api.useautumn.com/v1/check" \ - -H "Authorization: Bearer am_sk_1234567890" \ - -H "Content-Type: application/json" \ - -d '{ - "customer_id": "user_or_org_id_from_auth", - "feature_id": "premium_messages" - }' - \`\`\` - - - - \`\`\`json theme={null} - { - "customer_id": "user_or_org_id_from_auth", - "feature_id": "premium_messages", - "code": "feature_found", - "allowed": true, - "balance": 10, - "usage": 0, - "included_usage": 10, - "unlimited": false, - "interval": null, - "interval_count": 1, - "next_reset_at": null, - "overage_allowed": false - } - \`\`\` - - - - - #### Tracking premium messages - - Now let's implement our usage tracking and use up our premium messages. In this example, we're using 5 premium messages. - - - \`\`\`typescript Node.js theme={null} - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - await autumn.track({ - customer_id: "user_or_org_id_from_auth", - feature_id: "premium_messages", - value: 5, - }); - \`\`\` - - \`\`\`python Python theme={null} - import asyncio - from autumn import Autumn - - autumn = Autumn("am_sk_42424242") - - async def main(): - await autumn.track( - customer_id="user_or_org_id_from_auth", - feature_id="premium_messages", - value=5, - ) - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL theme={null} - curl -X POST "https://api.useautumn.com/v1/track" \ - -H "Authorization: Bearer am_sk_42424242" \ - -H "Content-Type: application/json" \ - -d '{ - "customer_id": "user_or_org_id_from_auth", - "feature_id": "premium_messages", - "value": 5 - }' - \`\`\` - - - - \`\`\`json theme={null} - { - "code": "event_received", - "customer_id": "user_or_org_id_from_auth", - "feature_id": "premium_messages" - } - \`\`\` - - - - - #### Purchasing top-ups - - When users run out of premium messages, they can purchase additional messages using our top-up plan. In this example, the user is purchasing 200 premium messages, which will cost them $20. - - - \`\`\`jsx React theme={null} - import { useCustomer, CheckoutDialog } from "autumn-js/react"; - - export default function TopUpButton() { - const { checkout } = useCustomer(); - - return ( - - ); - } - \`\`\` - - \`\`\`typescript Node.js theme={null} - import { Autumn } from "autumn-js"; - - const autumn = new Autumn({ - secretKey: 'am_sk_42424242', - }); - - const { data } = await autumn.checkout({ - customer_id: "user_or_org_id_from_auth", - product_id: "top_up", - options: [{ - feature_id: "premium_messages", - quantity: 200, - }], - }); - - if (data.url) { - // Redirect user to Stripe checkout URL - } else { - // Show purchase preview to user - } - \`\`\` - - \`\`\`python Python theme={null} - import asyncio - from autumn import Autumn - - autumn = Autumn("am_sk_42424242") - - async def main(): - response = await autumn.checkout( - customer_id="user_or_org_id_from_auth", - product_id="top-up", - options=[{ - "feature_id": "premium-messages", - "quantity": 200, - }], - ) - - if response.url: - # Redirect user to Stripe checkout URL - pass - else: - # Show purchase preview to user - pass - - asyncio.run(main()) - \`\`\` - - \`\`\`bash cURL theme={null} - curl -X POST "https://api.useautumn.com/v1/checkout" \ - -H "Authorization: Bearer am_sk_42424242" \ - -H "Content-Type: application/json" \ - -d '{ - "customer_id": "user_or_org_id_from_auth", - "product_id": "top-up", - "options": [{ - "feature_id": "premium-messages", - "quantity": 200 - }] - }' - \`\`\` - - - - \`\`\`json theme={null} - { - "customer_id": "user_or_org_id_from_auth", - "lines": [ - { - "description": "Top-up - 200 premium messages", - "amount": 20, - "item": { - "type": "feature", - "feature_id": "premium-messages", - "feature_type": "prepaid", - "feature": { - "id": "premium-messages", - "name": "Premium messages", - "type": "metered", - "display": { - "singular": "premium message", - "plural": "premium messages" - } - }, - "quantity": 200, - "price": 10, - "price_per": 100, - "display": { - "primary_text": "200 premium messages", - "secondary_text": "$10 per 100 messages" - } - } - } - ], - "product": { - "id": "top-up", - "name": "Top-up", - "group": null, - "env": "sandbox", - "is_add_on": false, - "is_default": false, - "archived": false, - "version": 1, - "created_at": 1766428038264, - "items": [ - { - "type": "feature", - "feature_id": "premium-messages", - "feature_type": "prepaid", - "feature": { - "id": "premium-messages", - "name": "Premium messages", - "type": "metered", - "display": { - "singular": "premium message", - "plural": "premium messages" - } - }, - "price": 10, - "price_per": 100, - "display": { - "primary_text": "$10 per 100 messages" - } - } - ], - "free_trial": null, - "base_variant_id": null, - "scenario": "attach", - "properties": { - "is_free": false, - "is_one_off": true, - "has_trial": false, - "updateable": false - } - }, - "total": 20, - "currency": "usd", - "url": "https://checkout.stripe.com/c/pay/.......", - "has_prorations": false - } - \`\`\` - - - Once the customer completes the payment, they will have an additional 200 premium messages available to use. You can display to the user by getting balances from the \`customer\` method. - -
- - ---- - -> To find navigation and other pages in this documentation, fetch the llms.txt file at: https://docs.useautumn.com/llms.txt - -`; diff --git a/packages/atmn/src/prompts/pricing.ts b/packages/atmn/src/prompts/pricing.ts deleted file mode 100644 index b19270265..000000000 --- a/packages/atmn/src/prompts/pricing.ts +++ /dev/null @@ -1,149 +0,0 @@ -export const pricingPrompt = `## Design your Autumn pricing model - -This guide helps you design your pricing model for Autumn. Autumn uses a configuration file (\`autumn.config.ts\`) to define your features and products (plans). - -### Step 1: Understand your pricing needs - -Before building, consider: -1. What features do you want to offer? (API calls, seats, storage, etc.) -2. What plans do you want? (Free, Pro, Enterprise tiers?) -3. How should usage be measured and limited? - ---- - -## Feature Types - -Autumn supports these feature types: - -- **single_use**: Consumable resources (API calls, tokens, messages, credits, generations) -- **continuous_use**: Non-consumable resources (seats, workspaces, projects, team members) -- **boolean**: On/off features (advanced analytics, priority support, SSO) -- **credit_system**: A unified credit pool that maps to multiple single_use features - ---- - -## Item Types - -Products contain an array of items. There are distinct item patterns: - -### 1. Flat Fee (standalone price, no feature) -\`\`\`typescript -{ feature_id: null, price: 13, interval: "month" } -\`\`\` -Customer pays $13/month as a base subscription fee. - -### 2. Free Feature Allocation (feature grant, no price) -\`\`\`typescript -{ feature_id: "credits", included_usage: 10000 } -\`\`\` -Customer gets 10,000 credits included. - -### 3. Metered/Usage-Based Pricing -\`\`\`typescript -{ feature_id: "credits", included_usage: 10000, price: 0.01, usage_model: "pay_per_use", interval: "month" } -\`\`\` -Customer can use 10,000 credits per month, then pays $0.01 per credit after that. - -### 4. Prepaid Credit Purchase (one-time purchase of usage) -\`\`\`typescript -{ feature_id: "credits", price: 10, usage_model: "prepaid", billing_units: 10000 } -\`\`\` -Customer pays $10 once to receive 10,000 credits. - -### 5. Tiered Pricing -\`\`\`typescript -{ feature_id: "api_calls", included_usage: 1000, tiers: [{ to: 5000, amount: 0.02 }, { to: "inf", amount: 0.01 }], usage_model: "pay_per_use", interval: "month" } -\`\`\` -Customer gets 1,000 API calls free, then pays $0.02/call up to 5,000, then $0.01/call after that. - -### 6. Per-Unit Pricing Structure -For any "per-X" pricing (like "$Y per seat", "$Y per project", "$Y per website"), use this pattern: -\`\`\`typescript -// Base subscription fee -{ feature_id: null, price: 10, interval: "month" } -// Unit allocation -{ feature_id: "seats", included_usage: 1, price: 10, usage_model: "pay_per_use", billing_units: 1 } -\`\`\` -This creates: $10/month base price that includes 1 unit, then $10 per additional unit purchased. - -**Always** use this two-item pattern for any per-unit pricing - never use pure per-unit without a base fee. - ---- - -## Guidelines - -### Naming Conventions -- Product and Feature IDs should be lowercase with underscores (e.g., \`pro_plan\`, \`chat_messages\`) - -### Features vs Plan Features -- Features define WHAT can be tracked (e.g., "credits"). Plan features define HOW a feature is granted in a plan (recurring, one-time, free, paid). -- Never create duplicate features for the same underlying resource. For example, "monthly tokens" and "one-time tokens" should be the SAME feature ("tokens"), referenced by different plan items with different intervals. - -### Default Plans -- **Never** set \`is_default: true\` for plans with prices. Default plans must be free. - -### Enterprise Plans -- Ignore "Enterprise" plans with custom pricing in the config. Custom plans can be created per-customer in the Autumn dashboard. - -### Annual Plans -- For annual variants, create a separate plan with annual price interval. Name it \` - Annual\`. - -### Currency -- Currency can be changed in the Autumn dashboard under Developer > Stripe. - ---- - -## Example Configuration - -\`\`\`typescript -import { feature, plan, item } from "atmn"; - -// Features -export const messages = feature({ - id: "messages", - name: "Messages", - type: "single_use", -}); - -export const seats = feature({ - id: "seats", - name: "Team Seats", - type: "continuous_use", -}); - -// Plans -export const free = plan({ - id: "free", - name: "Free", - is_default: true, - items: [ - item({ featureId: "messages", included: 100 }), - item({ featureId: "seats", included: 1 }), - ], -}); - -export const pro = plan({ - id: "pro", - name: "Pro", - items: [ - item({ featureId: null, price: 29, interval: "month" }), - item({ featureId: "messages", included: 10000, price: 0.01, usage_model: "pay_per_use" }), - item({ featureId: "seats", included: 5, price: 10, usage_model: "pay_per_use", billingUnits: 1 }), - ], -}); -\`\`\` - ---- - -## Next Steps - -Once you've designed your pricing: -1. Update \`autumn.config.ts\` with your features and plans -2. Run \`atmn push\` to sync your configuration to Autumn -3. Test in sandbox mode before going live - -For more help: https://discord.gg/atmn (we're very responsive) - -For questions about specific functionality or advanced use cases, check out the documentation: https://docs.useautumn.com - -Docs: https://docs.useautumn.com/llms.txt`; diff --git a/packages/atmn/src/prompts/skills/autumn-accepting-payments.ts b/packages/atmn/src/prompts/skills/autumn-accepting-payments.ts deleted file mode 100644 index 153c5521f..000000000 --- a/packages/atmn/src/prompts/skills/autumn-accepting-payments.ts +++ /dev/null @@ -1,346 +0,0 @@ -export default `--- -name: autumn-accepting-payments -description: | - Adds Autumn payment flow to a codebase including checkout, plan changes, and billing state. - Use this skill when: - - Adding payment or checkout flow with Autumn - - Implementing plan upgrades or downgrades - - Building pricing cards or subscription UI - - Adding billing state to display current plans - - Implementing cancel subscription functionality - - Adding prepaid top-ups or credit purchases - - User wants to "add payments" or "set up billing UI" ---- - -# Add Autumn Payment Flow - -Autumn handles Stripe checkout and plan changes. This skill guides you through adding the payment flow to a codebase for ALL plans in the Autumn configuration. - -## Step 1: Detect Integration Type - -Check if the codebase already has Autumn set up: - -- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted: **Path A: React** -- If there's just an \`Autumn\` client initialized: **Path B: Backend SDK** - -Before implementing: - -1. Tell the user which path you'll follow before proceeding. -2. Tell them you will be building pricing cards to handle billing flows, and ask for any guidance or input. - ---- - -## Path A: React - -### Checkout Flow - -Use \`checkout\` from \`useCustomer\`. It returns either a Stripe URL (new customer) or checkout preview data (returning customer with card on file). - -\`\`\`tsx -import { useCustomer } from "autumn-js/react"; - -const { checkout } = useCustomer(); - -const data = await checkout({ productId: "pro" }); - -if (!data.url) { - // Returning customer: show confirmation dialog with result data - // data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle } -} -\`\`\` - -After user confirms in your dialog, call \`attach\` to enable the plan (and charge card as needed): - -\`\`\`tsx -const { attach } = useCustomer(); - -await attach({ productId: "pro" }); -\`\`\` - -### Getting Billing State - -Use \`usePricingTable\` to get products with their billing scenario and display state. - -\`\`\`tsx -import { usePricingTable } from "autumn-js/react"; - -function PricingPage() { - const { products } = usePricingTable(); - // Each product has: scenario, properties - // scenario: "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel" -} -\`\`\` - -### Canceling - -Only use this if there is no free plan in the user's Autumn config. If there is a free plan, cancel by attaching the free plan instead. - -\`\`\`tsx -const { cancel } = useCustomer(); -await cancel({ productId: "pro" }); -\`\`\` - ---- - -## Path B: Backend SDK - -### Checkout Flow - -Payments are a 2-step process: - -1. **checkout** - Returns Stripe checkout URL (new customer) or preview data (returning customer) -2. **attach** - Confirms purchase when no URL was returned - -**TypeScript:** - -\`\`\`typescript -import { Autumn } from "autumn-js"; -import type { CheckoutResult, AttachResult } from "autumn-js"; - -const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY }); - -// Step 1: Get checkout info -const { data } = await autumn.checkout({ - customer_id: "user_or_org_id_from_auth", - product_id: "pro", -}) as { data: CheckoutResult }; - -if (data.url) { - // New customer: redirect to Stripe - return redirect(data.url); -} else { - // Returning customer: return preview data for confirmation UI - // data contains: { product, current_product, lines, total (IN MAJOR CURRENCY), currency, next_cycle } - return data; -} - -// Step 2: After user confirms (only if no URL) -const { data: attachData } = await autumn.attach({ - customer_id: "user_or_org_id_from_auth", - product_id: "pro", -}) as { data: AttachResult }; -\`\`\` - -**Python:** - -\`\`\`python -from autumn import Autumn - -autumn = Autumn('am_sk_test_xxx') - -# Step 1: Get checkout info -response = await autumn.checkout( - customer_id="user_or_org_id_from_auth", - product_id="pro", -) - -if response.url: - # New customer: redirect to Stripe - return redirect(response.url) -else: - # Returning customer: return preview data for confirmation UI - return response - -# Step 2: After user confirms -attach_response = await autumn.attach( - customer_id="user_or_org_id_from_auth", - product_id="pro", -) -\`\`\` - -### Getting Billing State - -Use \`products.list\` with a \`customer_id\` to get products with their billing scenario. **Don't build custom billing state logic.** - -**TypeScript:** - -\`\`\`typescript -const { data } = await autumn.products.list({ - customer_id: "user_or_org_id_from_auth", -}); - -data.list.forEach((product) => { - const { scenario } = product; - // "scheduled" | "active" | "new" | "renew" | "upgrade" | "downgrade" | "cancel" -}); -\`\`\` - -**Python:** - -\`\`\`python -response = await autumn.products.list(customer_id="user_or_org_id_from_auth") - -for product in response.list: - scenario = product.scenario -\`\`\` - -**curl:** - -\`\`\`bash -curl https://api.useautumn.com/v1/products?customer_id=user_or_org_id_from_auth \\ - -H "Authorization: Bearer $AUTUMN_SECRET_KEY" -\`\`\` - -### Canceling - -\`\`\`typescript -await autumn.cancel({ customer_id: "...", product_id: "pro" }); -\`\`\` - -Or attach a free product ID to downgrade. - ---- - -## Common Patterns - -### Pricing Button Text - -\`\`\`typescript -const SCENARIO_TEXT: Record = { - scheduled: "Plan Scheduled", - active: "Current Plan", - renew: "Renew", - upgrade: "Upgrade", - new: "Enable", - downgrade: "Downgrade", - cancel: "Cancel Plan", -}; - -export const getPricingButtonText = (product: Product): string => { - const { scenario, properties } = product; - const { is_one_off, updateable, has_trial } = properties ?? {}; - - if (has_trial) return "Start Trial"; - if (scenario === "active" && updateable) return "Update"; - if (scenario === "new" && is_one_off) return "Purchase"; - - return SCENARIO_TEXT[scenario ?? ""] ?? "Enable Plan"; -}; -\`\`\` - -### Confirmation Dialog Text - -\`\`\`typescript -import type { CheckoutResult, Product } from "autumn-js"; - -export const getConfirmationTexts = (result: CheckoutResult): { title: string; message: string } => { - const { product, current_product, next_cycle } = result; - const scenario = product.scenario; - const productName = product.name; - const currentProductName = current_product?.name; - const nextCycleDate = next_cycle?.starts_at - ? new Date(next_cycle.starts_at).toLocaleDateString() - : undefined; - - const isRecurring = !product.properties?.is_one_off; - - const CONFIRMATION_TEXT: Record = { - scheduled: { title: "Already Scheduled", message: "You already have this product scheduled." }, - active: { title: "Already Active", message: "You are already subscribed to this product." }, - renew: { title: "Renew", message: \`Renew your subscription to \${productName}.\` }, - upgrade: { title: \`Upgrade to \${productName}\`, message: \`Upgrade to \${productName}. Your card will be charged immediately.\` }, - downgrade: { title: \`Downgrade to \${productName}\`, message: \`\${currentProductName} will be cancelled. \${productName} begins \${nextCycleDate}.\` }, - cancel: { title: "Cancel", message: \`Your subscription to \${currentProductName} will end \${nextCycleDate}.\` }, - }; - - if (scenario === "new") { - return isRecurring - ? { title: \`Subscribe to \${productName}\`, message: \`Subscribe to \${productName}. Charged immediately.\` } - : { title: \`Purchase \${productName}\`, message: \`Purchase \${productName}. Charged immediately.\` }; - } - - return CONFIRMATION_TEXT[scenario ?? ""] ?? { title: "Change Subscription", message: "You are about to change your subscription." }; -}; -\`\`\` - ---- - -## Prepaid Top-ups Reference - -Let customers purchase prepaid packages and top-ups. If a user hits a usage limit, they may be willing to purchase a top-up. - -These are typically one-time purchases (or less commonly, recurring add-ons) that grant a fixed usage of a feature. - -### Purchasing top-ups (React) - -\`\`\`tsx -import { useCustomer, CheckoutDialog } from "autumn-js/react"; - -export default function TopUpButton() { - const { checkout } = useCustomer(); - - return ( - - ); -} -\`\`\` - -### Purchasing top-ups (Backend) - -**TypeScript:** - -\`\`\`typescript -const { data } = await autumn.checkout({ - customer_id: "user_or_org_id_from_auth", - product_id: "top_up", - options: [{ - feature_id: "premium_messages", - quantity: 200, - }], -}); - -if (data.url) { - // Redirect user to Stripe checkout URL -} else { - // Show purchase preview to user -} -\`\`\` - -**Python:** - -\`\`\`python -response = await autumn.checkout( - customer_id="user_or_org_id_from_auth", - product_id="top-up", - options=[{ - "feature_id": "premium-messages", - "quantity": 200, - }], -) - -if response.url: - # Redirect user to Stripe checkout URL - pass -else: - # Show purchase preview to user - pass -\`\`\` - ---- - -## Important Notes - -- **The result is \`data.url\`, NOT \`data.checkout_url\`** -- This handles all upgrades, downgrades, renewals, and uncancellations automatically -- Product IDs come from the Autumn configuration -- Pass \`successUrl\` to \`checkout\` to redirect users after payment -- Your Autumn configuration is in \`autumn.config.ts\` in your project root - -## Resources - -- Documentation: https://docs.useautumn.com -- LLM-friendly docs: https://docs.useautumn.com/llms.txt -`; diff --git a/packages/atmn/src/prompts/skills/autumn-billing-page.ts b/packages/atmn/src/prompts/skills/autumn-billing-page.ts new file mode 100644 index 000000000..c758b786b --- /dev/null +++ b/packages/atmn/src/prompts/skills/autumn-billing-page.ts @@ -0,0 +1,514 @@ +export default `--- +name: autumn-billing-page +description: | + Build a billing page and manage subscriptions with Autumn. + Use this skill when the user wants to: + - Display active plans or subscription status + - Show usage balances to customers + - Build a pricing page with upgrade/downgrade buttons + - Implement plan switching (upgrades/downgrades) + - Add cancel/uncancel subscription functionality + - Open the Stripe billing portal + - Display usage history charts + - Add prepaid top-ups or credit purchases +--- + +# Build Your Billing Page + +Software applications typically ship with a billing page. This allows customers to change plan, cancel subscription and view their usage. + +> Your Autumn configuration is in \`autumn.config.ts\`. If it doesn't exist, run \`npx atmn init\` to log in and generate the file. + +## Step 1: Detect Integration Type + +Check if the codebase already has Autumn set up: + +- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted: **Path A: React** +- If there's just an \`Autumn\` client initialized: **Path B: Backend SDK** + +Before implementing: + +1. Tell the user which path you'll follow before proceeding. +2. Tell them you will be building billing page components, and ask for any guidance or input. + +--- + +## Active Plans + +Display the plan the user is currently on. Users can have multiple active subscriptions and purchases (e.g., main plan and add-ons). + +- **\`subscriptions\`** - Free and paid recurring plans +- **\`purchases\`** - One-off plans (e.g., credit top-ups) + +### React + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +const { data: customer } = useCustomer(); + +const active = customer?.subscriptions.filter( + (sub) => sub.status === "active" +); + +console.log(active?.map((sub) => sub.planId).join(", ")); +\`\`\` + +### TypeScript + +\`\`\`typescript +import { Autumn } from "autumn-js"; + +const autumn = new Autumn({ secretKey: "am_sk_test_xxx" }); + +const customer = await autumn.customers.getOrCreate({ + customerId: "user_123", +}); + +const active = customer.subscriptions?.filter( + (sub) => sub.status === "active" +); + +console.log(active?.map((sub) => sub.planId).join(", ")); +\`\`\` + +### Python + +\`\`\`python +from autumn_sdk import Autumn + +autumn = Autumn("am_sk_test_xxx") + +customer = await autumn.customers.get_or_create( + customer_id="user_123" +) + +active = [s for s in customer.subscriptions if s.status == "active"] +print([s.plan_id for s in active]) +\`\`\` + +--- + +## Usage Balances + +Metered features have \`granted\`, \`usage\`, and \`remaining\` fields. Use these to display current usage and remaining balance. + +### React + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +const { data: customer, refetch } = useCustomer(); + +const messages = customer?.balances.messages; + +console.log(\\\`\\\${messages?.remaining} / \\\${messages?.granted}\\\`); + +// After tracking usage or changing plans, call refetch() to update balances +await refetch(); +\`\`\` + +### TypeScript + +\`\`\`typescript +const customer = await autumn.customers.getOrCreate({ + customerId: "user_123", +}); + +const messages = customer.balances?.messages; +console.log(\\\`\\\${messages?.remaining} / \\\${messages?.granted}\\\`); +\`\`\` + +### Python + +\`\`\`python +customer = await autumn.customers.get_or_create( + customer_id="user_123" +) + +messages = customer.balances.get("messages") +print(f"{messages.remaining} / {messages.granted}") +\`\`\` + +--- + +## Customer Eligibility + +When building a pricing page, you need to know what each plan means for the current customer -- is it an upgrade, a downgrade, or their current plan? Is a free trial available? + +Pass a \`customerId\` when listing plans and each plan will include a \`customerEligibility\` object: + +- **\`attachAction\`** -- What happens when this plan is attached: \`"activate"\`, \`"upgrade"\`, \`"downgrade"\`, \`"purchase"\`, or \`"none"\` +- **\`status\`** -- The customer's current relationship to this plan: \`"active"\`, \`"scheduled"\`, or undefined if none +- **\`trialAvailable\`** -- Whether the customer is eligible for the plan's free trial + +### React + +\`\`\`tsx +import { useListPlans, useCustomer } from "autumn-js/react"; + +const labels = { + activate: "Subscribe", + upgrade: "Upgrade", + downgrade: "Downgrade", + purchase: "Purchase", +}; + +const getLabel = (eligibility) => { + + if (eligibility?.attachAction === "none") { + return eligibility.status === "scheduled" ? "Plan Scheduled" : "Current plan"; + } + + if (labels[eligibility?.attachAction]) { + return labels[eligibility.attachAction]; + } + + return "Get started"; +}; + +export default function PricingPage() { + const { data: plans } = useListPlans(); + const { attach } = useCustomer(); + + return plans?.map((plan) => ( + + )); +} +\`\`\` + +The React \`useListPlans\` hook automatically includes customer context from \`AutumnProvider\`, so \`customerEligibility\` is populated on every plan without extra configuration. + +### TypeScript + +\`\`\`typescript +const { list: plans } = await autumn.plans.list({ + customerId: "user_123", +}); + +for (const plan of plans) { + console.log(plan.name, plan.customerEligibility?.attachAction); + // e.g. "Free" "downgrade", "Pro" "none", "Enterprise" "upgrade" +} +\`\`\` + +### Python + +\`\`\`python +plans = await autumn.plans.list(customer_id="user_123") + +for plan in plans.list: + print(plan.name, plan.customer_eligibility.attach_action) +\`\`\` + +--- + +## Switching Plans + +Use \`attach\` to switch between plans. This handles upgrades, downgrades, and new subscriptions. + +### React + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +export default function UpgradeButton() { + const { attach } = useCustomer(); + + return ( + + ); +} +\`\`\` + +### TypeScript + +\`\`\`typescript +const response = await autumn.billing.attach({ + customerId: "user_123", + planId: "pro", +}); + +redirect(response.paymentUrl); +\`\`\` + +### Python + +\`\`\`python +response = await autumn.billing.attach( + customer_id="user_123", + plan_id="pro", +) +# Redirect to response.payment_url +\`\`\` + +--- + +## Cancelling a Plan + +Cancel a subscription using \`billing.update\` with a \`cancelAction\`. + +### React + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +const { updateSubscription } = useCustomer(); + +// Cancel at end of billing cycle +await updateSubscription({ + planId: "pro", + cancelAction: "cancel_end_of_cycle", +}); +\`\`\` + +### TypeScript + +\`\`\`typescript +await autumn.billing.update({ + customerId: "user_123", + planId: "pro", + cancelAction: "cancel_end_of_cycle", +}); +\`\`\` + +### Python + +\`\`\`python +await autumn.billing.update( + customer_id="user_123", + plan_id="pro", + cancel_action="cancel_end_of_cycle", +) +\`\`\` + +--- + +## Uncancelling a Plan + +If a subscription has a pending cancellation (when \`canceledAt\` is not null while the subscription is still \`active\`), you can reverse it: + +### React + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +export default function BillingPage() { + const { data: customer, updateSubscription } = useCustomer(); + + const cancellingSub = customer?.subscriptions.find( + (sub) => sub.status === "active" && sub.canceledAt !== null + ); + + return cancellingSub ? ( + + ) : null; +} +\`\`\` + +### TypeScript + +\`\`\`typescript +const customer = await autumn.customers.getOrCreate({ + customerId: "user_123", +}); + +const cancellingSub = customer.subscriptions?.find( + (sub) => sub.status === "active" && sub.canceledAt !== null +); + +if (cancellingSub) { + await autumn.billing.update({ + customerId: "user_123", + planId: cancellingSub.planId, + cancelAction: "uncancel", + }); +} +\`\`\` + +### Python + +\`\`\`python +customer = await autumn.customers.get_or_create( + customer_id="user_123" +) + +cancelling_sub = next( + (s for s in customer.subscriptions + if s.status == "active" and s.canceled_at is not None), + None, +) + +if cancelling_sub: + await autumn.billing.update( + customer_id="user_123", + plan_id=cancelling_sub.plan_id, + cancel_action="uncancel", + ) +\`\`\` + +--- + +## Stripe Billing Portal + +The Stripe billing portal lets users manage their payment method, view past invoices, and cancel their plan. Enable the billing portal in your Stripe settings first. + +### React + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +const { openCustomerPortal } = useCustomer(); + +await openCustomerPortal({ + returnUrl: "https://your-app.com/billing" +}); +\`\`\` + +### TypeScript + +\`\`\`typescript +const { url } = await autumn.billing.openCustomerPortal({ + customerId: "user_123", + returnUrl: "https://your-app.com/billing", +}); + +redirect(url); +\`\`\` + +### Python + +\`\`\`python +response = await autumn.billing.open_customer_portal( + customer_id="user_123", + return_url="https://your-app.com/billing", +) +# Redirect to response.url +\`\`\` + +--- + +## Usage History Chart + +Autumn provides aggregate time series queries for usage data. Pass the response to a charting library like Recharts. + +### React + +\`\`\`tsx +import { useAggregateEvents } from "autumn-js/react"; + +const { list, total } = useAggregateEvents({ + featureId: "messages", + range: "30d", +}); + +// list: [{ period: 1234567890, values: { messages: 42 } }, ...] +// total: { messages: { count: 100, sum: 500 } } +\`\`\` + +### TypeScript + +\`\`\`typescript +const { list, total } = await autumn.events.aggregate({ + customerId: "user_123", + featureId: "messages", + range: "30d", +}); +\`\`\` + +### Python + +\`\`\`python +response = await autumn.events.aggregate( + customer_id="user_123", + feature_id="messages", + range="30d", +) +# response.list, response.total +\`\`\` + +--- + +## Prepaid Top-ups Reference + +Let customers purchase prepaid packages and top-ups. If a user hits a usage limit, they may be willing to purchase a top-up. These are typically one-time purchases that grant a fixed amount of usage. + +### React + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +export default function TopUpButton() { + const { attach } = useCustomer(); + + return ( + + ); +} +\`\`\` + +### TypeScript + +\`\`\`typescript +const response = await autumn.billing.attach({ + customerId: "user_or_org_id_from_auth", + planId: "top_up", + options: [{ + featureId: "messages", + quantity: 200, + }], +}); + +if (response.paymentUrl) { + redirect(response.paymentUrl); +} +\`\`\` + +### Python + +\`\`\`python +response = await autumn.billing.attach( + customer_id="user_or_org_id_from_auth", + plan_id="top_up", + options=[{ + "feature_id": "messages", + "quantity": 200, + }], +) +\`\`\` + +--- + +## Important Notes + +- This handles all upgrades, downgrades, renewals, and uncancellations automatically +- Plan IDs come from the Autumn configuration +- Your Autumn configuration is in \`autumn.config.ts\` in your project root + +**Docs:** https://docs.useautumn.com/llms.txt +`; diff --git a/packages/atmn/src/prompts/skills/autumn-creating-customers.ts b/packages/atmn/src/prompts/skills/autumn-creating-customers.ts deleted file mode 100644 index 3acff4ca5..000000000 --- a/packages/atmn/src/prompts/skills/autumn-creating-customers.ts +++ /dev/null @@ -1,334 +0,0 @@ -export default `--- -name: autumn-creating-customers -description: | - Sets up Autumn billing integration by creating an Autumn customer in a codebase. - Use this skill when the user wants to: - - Set up Autumn billing - - Create an Autumn customer - - Integrate Autumn into their app - - Add billing/entitlements with Autumn - - Configure Autumn SDK ---- - -# Set up Autumn Billing Integration - -Autumn is a billing and entitlements layer over Stripe. This skill guides you through creating an Autumn customer and adding it to a place in the app where it will be automatically created. - -## Step 1: Analyze the Codebase - -Before making changes, detect: - -- **Language**: TypeScript/JavaScript, Python, or other -- **If TS/JS - Framework**: Next.js, React Router, Tanstack Start, Hono, Express, Fastify, or other -- **If TS/JS - React frontend?**: Check for React in package.json - -Then ask the user: - -1. **Should Autumn customers be individual users, or organizations?** - - **Users (B2C)**: Each user has their own plan and limits - - **Organizations (B2B)**: Plans and limits are shared across an org - -2. **Have you created an AUTUMN_SECRET_KEY and added it to .env?** - - Prompt them to create one at: https://app.useautumn.com/dev?tab=api_keys - - Add it to \`.env\` as \`AUTUMN_SECRET_KEY\` - -Tell the user what you detected, which path you'll follow, and what you'll be adding Autumn to. - ---- - -## Path A: React + Node.js (Fullstack TypeScript) - -Use this path if there's a React frontend with a Node.js backend. - -### A1. Install the SDK - -Use the package manager already installed (npm, yarn, pnpm, bun): - -\`\`\`bash -npm install autumn-js -\`\`\` - -### A2. Mount the Handler (Server-Side) - -This creates endpoints at \`/api/autumn/*\` that the React hooks will call. The \`identify\` function should return either the user ID or org ID from your auth provider, depending on how you're using Autumn. - -#### Next.js (App Router) - -\`\`\`typescript -// app/api/autumn/[...all]/route.ts -import { autumnHandler } from "autumn-js/next"; - -export const { GET, POST } = autumnHandler({ - identify: async (request) => { - // Get user/org from your auth provider - const session = await auth.api.getSession({ headers: request.headers }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { - name: session?.user.name, - email: session?.user.email, - }, - }; - }, -}); -\`\`\` - -#### React Router - -\`\`\`typescript -// app/routes/api.autumn.tsx -import { autumnHandler } from "autumn-js/react-router"; - -export const { loader, action } = autumnHandler({ - identify: async (args) => { - const session = await auth.api.getSession({ headers: args.request.headers }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, -}); - -// routes.ts - add this route -route("api/autumn/*", "routes/api.autumn.tsx") -\`\`\` - -#### Tanstack Start - -\`\`\`typescript -// routes/api/autumn.$.ts -import { autumnHandler } from "autumn-js/tanstack"; - -const handler = autumnHandler({ - identify: async ({ request }) => { - const session = await auth.api.getSession({ headers: request.headers }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, -}); - -export const Route = createFileRoute("/api/autumn/$")({ - server: { handlers: handler }, -}); -\`\`\` - -#### Hono - -\`\`\`typescript -import { autumnHandler } from "autumn-js/hono"; - -app.use("/api/autumn/*", autumnHandler({ - identify: async (c) => { - const session = await auth.api.getSession({ headers: c.req.raw.headers }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, -})); -\`\`\` - -#### Express - -\`\`\`typescript -import { autumnHandler } from "autumn-js/express"; - -app.use(express.json()); // Must be before autumnHandler -app.use("/api/autumn", autumnHandler({ - identify: async (req) => { - const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, -})); -\`\`\` - -#### Fastify - -\`\`\`typescript -import { autumnHandler } from "autumn-js/fastify"; - -fastify.route({ - method: ["GET", "POST"], - url: "/api/autumn/*", - handler: autumnHandler({ - identify: async (request) => { - const session = await auth.api.getSession({ headers: request.headers as any }); - return { - customerId: session?.user.id, // or session?.org.id for B2B - customerData: { name: session?.user.name, email: session?.user.email }, - }; - }, - }), -}); -\`\`\` - -#### Other Frameworks (Generic Handler) - -\`\`\`typescript -import { autumnHandler } from "autumn-js/backend"; - -// Mount this handler onto the /api/autumn/* path in your backend -const handleRequest = async (request) => { - // Your authentication logic here - const customerId = "user_or_org_id_from_auth"; - - let body = null; - if (request.method !== "GET") { - body = await request.json(); - } - - const { statusCode, response } = await autumnHandler({ - customerId, - customerData: { name: "", email: "" }, - request: { - url: request.url, - method: request.method, - body: body, - }, - }); - - return new Response(JSON.stringify(response), { - status: statusCode, - headers: { "Content-Type": "application/json" }, - }); -}; -\`\`\` - -### A3. Add the Provider (Client-Side) - -Wrap your app with \`AutumnProvider\`: - -\`\`\`tsx -import { AutumnProvider } from "autumn-js/react"; - -export default function RootLayout({ children }) { - return ( - - {children} - - ); -} -\`\`\` - -If your backend is on a different URL (e.g., Vite + separate server), pass \`backendUrl\`: - -\`\`\`tsx - -\`\`\` - -### A4. Create a Test Customer - -Add this hook to any component to verify the integration: - -\`\`\`tsx -import { useCustomer } from "autumn-js/react"; - -const { customer } = useCustomer(); -console.log("Autumn customer:", customer); -\`\`\` - -This automatically creates an Autumn customer for new users/orgs. - ---- - -## Path B: Backend Only (Node.js, Python, or Other) - -Use this path if there's no React frontend, or you prefer server-side only. - -### B1. Install the SDK - -**Node.js:** - -\`\`\`bash -npm install autumn-js -\`\`\` - -**Python:** - -\`\`\`bash -pip install autumn-py -\`\`\` - -### B2. Initialize the Client - -**TypeScript/JavaScript:** - -\`\`\`typescript -import { Autumn } from "autumn-js"; - -const autumn = new Autumn({ - secretKey: process.env.AUTUMN_SECRET_KEY, -}); -\`\`\` - -**Python:** - -\`\`\`python -from autumn import Autumn - -autumn = Autumn('am_sk_test_xxx') -\`\`\` - -### B3. Create a Test Customer - -This will GET or CREATE a new customer. Add it when a user signs in or loads the app. Pass in ID from auth provider. - -The response returns customer state, used to display billing information client-side. Log the Autumn customer client-side. - -**TypeScript:** - -\`\`\`typescript -const { data, error } = await autumn.customers.create({ - id: "user_or_org_id_from_auth", - name: "Test User", - email: "test@example.com", -}); -\`\`\` - -**Python:** - -\`\`\`python -customer = await autumn.customers.create( - id="user_or_org_id_from_auth", - name="Test User", - email="test@example.com", -) -\`\`\` - -**cURL:** - -\`\`\`bash -curl -X POST https://api.useautumn.com/customers \\ - -H "Authorization: Bearer am_sk_test_xxx" \\ - -H "Content-Type: application/json" \\ - -d '{"id": "user_or_org_id_from_auth", "name": "Test User", "email": "test@example.com"}' -\`\`\` - -### Type Safety - -When calling these functions from the client, the SDK exports types for all response objects: - -\`\`\`tsx -import type { Customer } from "autumn-js"; -\`\`\` - ---- - -## Verification - -After setup, report to the user: - -1. What stack you detected -2. Which path you followed -3. What files you created/modified -4. That the Autumn customer is logged in browser, and to check in the Autumn dashboard - -**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root. - -**Documentation:** https://docs.useautumn.com/llms.txt -`; diff --git a/packages/atmn/src/prompts/skills/autumn-tracking-metered-usage.ts b/packages/atmn/src/prompts/skills/autumn-gating.ts similarity index 55% rename from packages/atmn/src/prompts/skills/autumn-tracking-metered-usage.ts rename to packages/atmn/src/prompts/skills/autumn-gating.ts index bca164853..4832484a2 100644 --- a/packages/atmn/src/prompts/skills/autumn-tracking-metered-usage.ts +++ b/packages/atmn/src/prompts/skills/autumn-gating.ts @@ -1,7 +1,8 @@ export default `--- -name: autumn-tracking-metered-usage +name: autumn-gating description: | - Add usage tracking and feature gating with Autumn SDK. Use this skill when asked to: + Add usage tracking and feature gating with the Autumn SDK. + Use this skill when asked to: - Add usage tracking or metering - Implement feature limits or gating - Check feature access or entitlements @@ -11,9 +12,14 @@ description: | - Enforce usage limits server-side --- -# Autumn Usage & Gating +# Checking and Tracking Usage -Autumn tracks feature usage and enforces limits. This skill covers adding usage tracking and gating to a codebase. +Autumn handles your customer's payments and grants them the features defined in your plan configuration. There are 2 functions you need to enforce limits and gating: + +- \`check\` for feature access, before allowing a user to do something +- \`track\` the usage in Autumn afterwards (if needed) + +> Your Autumn configuration is in \`autumn.config.ts\`. If it doesn't exist, run \`npx atmn init\` to log in and generate the file. ## Step 1: Detect Integration Type @@ -26,39 +32,15 @@ Report what you detected before proceeding. --- -## Frontend Checks (React Hooks) +## Checking Feature Access -Use frontend checks for **UX only** - showing/hiding features, prompting upgrades. These should NOT be trusted for security. +Check if a user has enough remaining balance before executing an action. The \`feature_id\` used here is defined by you when you create the feature in Autumn. -### Check Feature Access - -\`\`\`tsx -import { useCustomer } from "autumn-js/react"; - -export function SendChatMessage() { - const { check, refetch } = useCustomer(); - - const handleSendMessage = async () => { - const { data } = check({ featureId: "messages" }); - - if (!data?.allowed) { - alert("You're out of messages"); - } else { - // send chatbot message - // then, refresh customer usage data - await refetch(); - } - }; -} -\`\`\` - ---- - -## Backend Checks (Required for Security) +### Backend Check (Required for Security) **Always check on the backend** before executing any protected action. Frontend checks can be bypassed. -### TypeScript +**TypeScript:** \`\`\`typescript import { Autumn } from "autumn-js"; @@ -67,58 +49,115 @@ const autumn = new Autumn({ secretKey: process.env.AUTUMN_SECRET_KEY, }); -// Check before executing the action -const { data } = await autumn.check({ - customer_id: "user_or_org_id_from_auth", - feature_id: "api_calls", +const { allowed } = await autumn.check({ + customerId: "user_or_org_id_from_auth", + featureId: "messages", + requiredBalance: 1, }); -if (!data.allowed) { - return { error: "Usage limit reached" }; +if (!allowed) { + console.log("User has run out of messages"); + return; } - -// Safe to proceed - do the actual work here -const result = await doTheActualWork(); - -// Track usage after success -await autumn.track({ - customer_id: "user_or_org_id_from_auth", - feature_id: "api_calls", - value: 1, -}); - -return result; \`\`\` -### Python +**Python:** \`\`\`python -from autumn import Autumn +from autumn_sdk import Autumn autumn = Autumn('am_sk_test_xxx') -# Check before executing the action response = await autumn.check( customer_id="user_or_org_id_from_auth", - feature_id="api_calls" + feature_id="messages", + required_balance=1, ) if not response.allowed: raise HTTPException(status_code=403, detail="Usage limit reached") +\`\`\` -# Safe to proceed - do the actual work here -result = await do_the_actual_work() +**cURL:** -# Track usage after success +\`\`\`bash +curl -X POST 'https://api.useautumn.com/v1/check' \\ + -H 'Authorization: Bearer am_sk_test_xxx' \\ + -H 'Content-Type: application/json' \\ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "feature_id": "messages", + "required_balance": 1 + }' +\`\`\` + +You can also use \`check\` to gate boolean features (non-metered features), such as access to "premium AI models". + +### Frontend Check (React Hooks - UX Only) + +When using React hooks, you have access to the customer object which you can use to display billing data. You can use the client-side \`check\` function to gate features and show paywalls. Permissions are determined by reading the local \`data\` state, so no call to Autumn's API is made. + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +export function SendChatMessage() { + const { check, refetch } = useCustomer(); + + const handleSendMessage = async () => { + const { allowed } = check({ featureId: "messages" }); + + if (!allowed) { + alert("You're out of messages"); + } else { + // Send chatbot message + // Then refresh customer usage data + await refetch(); + } + }; +} +\`\`\` + +--- + +## Tracking Usage + +After the user has successfully used a feature, record the usage in Autumn. This will decrement their balance. + +**TypeScript:** + +\`\`\`typescript +await autumn.track({ + customerId: "user_or_org_id_from_auth", + featureId: "messages", + value: 1, +}); +\`\`\` + +**Python:** + +\`\`\`python await autumn.track( customer_id="user_or_org_id_from_auth", - feature_id="api_calls", - value=1 + feature_id="messages", + value=1, ) - -return result \`\`\` +**cURL:** + +\`\`\`bash +curl -X POST 'https://api.useautumn.com/v1/track' \\ + -H 'Authorization: Bearer am_sk_test_xxx' \\ + -H 'Content-Type: application/json' \\ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "feature_id": "messages", + "value": 1 + }' +\`\`\` + +You should always handle access checks and usage tracking server-side for security. Users can manipulate client-side code using devtools. + --- ## Key Concepts @@ -129,18 +168,6 @@ return result - Feature IDs come from the Autumn configuration - Current usage and total limit are available from the Customer object -### Displaying Usage Info - -\`\`\`tsx -import type { Customer } from "autumn-js"; - -// Balance is: customer.features..balance -\`\`\` - -**Note:** Autumn configuration is typically in \`autumn.config.ts\` in the project root. - -**Docs:** https://docs.useautumn.com/llms.txt - --- ## Credit Systems Reference @@ -174,9 +201,9 @@ export function CheckBasicMessage() { const { check, refetch } = useCustomer(); const handleCheckAccess = async () => { - const { data } = await check({ featureId: "basic_messages", requiredBalance: 1 }); + const { allowed } = check({ featureId: "basic_messages", requiredBalance: 1 }); - if (!data?.allowed) { + if (!allowed) { alert("You've run out of basic message credits"); } else { // proceed with sending message @@ -189,13 +216,13 @@ export function CheckBasicMessage() { #### TypeScript \`\`\`typescript -const { data } = await autumn.check({ - customer_id: "user_or_org_id_from_auth", - feature_id: "basic_messages", - required_balance: 1, +const { allowed } = await autumn.check({ + customerId: "user_or_org_id_from_auth", + featureId: "basic_messages", + requiredBalance: 1, }); -if (!data.allowed) { +if (!allowed) { console.log("User has run out of basic message credits"); return; } @@ -219,11 +246,17 @@ if not response.allowed: \`\`\`typescript await autumn.track({ - customer_id: "user_or_org_id_from_auth", - feature_id: "basic_messages", + customerId: "user_or_org_id_from_auth", + featureId: "basic_messages", value: 2, }); \`\`\` This uses 2 basic messages, which costs 0.02 USD credits. + +--- + +**Note:** Autumn configuration is typically in \`autumn.config.ts\` in the project root. + +**Docs:** https://docs.useautumn.com/llms.txt `; diff --git a/packages/atmn/src/prompts/skills/autumn-modelling-pricing-plans.ts b/packages/atmn/src/prompts/skills/autumn-modelling-pricing-plans.ts index e75196673..26b47fe02 100644 --- a/packages/atmn/src/prompts/skills/autumn-modelling-pricing-plans.ts +++ b/packages/atmn/src/prompts/skills/autumn-modelling-pricing-plans.ts @@ -7,108 +7,472 @@ description: | - Creating autumn.config.ts configuration - Setting up usage-based, subscription, or credit-based pricing - Configuring features like API calls, seats, storage, or credits - - Understanding Autumn feature types (single_use, continuous_use, boolean, credit_system) + - Understanding Autumn feature types (metered, boolean, credit_system) - Working with plan items, metered billing, or tiered pricing --- # Autumn Pricing Model Design -This guide helps you design your pricing model for Autumn. Autumn uses a configuration file (\`autumn.config.ts\`) to define your features and products (plans). +This guide helps you design your pricing model for Autumn. Autumn uses a configuration file (\`autumn.config.ts\`) to define your features and plans. + +> **Before starting:** Check for an \`autumn.config.ts\` in the project root. If it doesn't exist, run \`npx atmn init\` to log in and generate the file. If you already have a config you want to modify, run \`atmn pull\` to sync it from Autumn first. ## Step 1: Understand Your Pricing Needs Before building, consider: 1. What features do you want to offer? (API calls, seats, storage, etc.) -2. What plans do you want? (Free, Pro, Enterprise tiers?) +2. What plans do you want? (Free, Pro, etc.) 3. How should usage be measured and limited? -## Feature Types +--- -Autumn supports these feature types: +## Features -| Type | Description | Examples | -|------|-------------|----------| -| \`single_use\` | Consumable resources | API calls, tokens, messages, credits, generations | -| \`continuous_use\` | Non-consumable resources | Seats, workspaces, projects, team members | -| \`boolean\` | On/off features | Advanced analytics, priority support, SSO | -| \`credit_system\` | Unified credit pool that maps to multiple single_use features | Credits redeemable for various actions | +Features define what can be gated, metered, or billed in your app. -## Item Types +### \`feature(config)\` -Products contain an array of items. There are distinct item patterns: +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| \`id\` | string | Yes | Unique identifier used in API calls (\`check\`, \`track\`, etc). | +| \`name\` | string | Yes | Display name shown in the dashboard and billing UI. | +| \`type\` | enum | Yes | \`"boolean"\` \\| \`"metered"\` \\| \`"credit_system"\` | +| \`consumable\` | boolean | For metered | \`true\` = consumed (messages, API calls), \`false\` = ongoing (seats, storage). | +| \`eventNames\` | string[] | No | Event names that trigger this feature. Allows multiple features to respond to a single event. | +| \`creditSchema\` | array | For credit_system | Maps metered features to credit costs. Each entry: \`{ meteredFeatureId, creditCost }\`. | -### 1. Flat Fee (standalone price, no feature) +### Feature Types + +**Boolean** -- simple on/off flag: \`\`\`typescript -{ feature_id: null, price: 13, interval: "month" } +export const sso = feature({ + id: 'sso', + name: 'SSO Authentication', + type: 'boolean', +}); \`\`\` -Customer pays $13/month as a base subscription fee. - -### 2. Free Feature Allocation (feature grant, no price) +**Metered, consumable** -- used up and replenished (messages, API calls): \`\`\`typescript -{ feature_id: "credits", included_usage: 10000 } +export const messages = feature({ + id: 'messages', + name: 'Messages', + type: 'metered', + consumable: true, +}); \`\`\` -Customer gets 10,000 credits included. - -### 3. Metered/Usage-Based Pricing +**Metered, non-consumable** -- ongoing usage (seats, storage): \`\`\`typescript -{ feature_id: "credits", included_usage: 10000, price: 0.01, usage_model: "pay_per_use", interval: "month" } +export const seats = feature({ + id: 'seats', + name: 'Seats', + type: 'metered', + consumable: false, +}); \`\`\` -Customer can use 10,000 credits per month, then pays $0.01 per credit after that. - -### 4. Prepaid Credit Purchase (one-time purchase of usage) +**Credit system** -- maps multiple metered features to credit costs: \`\`\`typescript -{ feature_id: "credits", price: 10, usage_model: "prepaid", billing_units: 10000 } +export const basicModel = feature({ + id: 'basic_model', + name: 'Basic Model', + type: 'metered', + consumable: true, +}); + +export const premiumModel = feature({ + id: 'premium_model', + name: 'Premium Model', + type: 'metered', + consumable: true, +}); + +export const credits = feature({ + id: 'credits', + name: 'AI Credits', + type: 'credit_system', + creditSchema: [ + { meteredFeatureId: basicModel.id, creditCost: 1 }, + { meteredFeatureId: premiumModel.id, creditCost: 5 }, + ], +}); \`\`\` -Customer pays $10 once to receive 10,000 credits. +If you set the price per credit to 1 cent, credits become monetary credits (eg, 5 credits = $0.05 per premium message). -### 5. Tiered Pricing +--- + +## Plans + +Plans combine features with pricing to create your subscription tiers, add-ons, and top-ups. + +### \`plan(config)\` + +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| \`id\` | string | Yes | Unique identifier used in checkout and subscription APIs. | +| \`name\` | string | Yes | Display name shown in pricing tables and billing. | +| \`price\` | object | No | Base subscription price: \`{ amount, interval }\`. | +| \`items\` | array | No | Array of \`item()\` objects defining what's included. | +| \`autoEnable\` | boolean | No | Automatically assign to new customers. Typically used for free plans. | +| \`addOn\` | boolean | No | Allow purchase alongside other plans (instead of replacing them). | +| \`freeTrial\` | object | No | \`{ durationLength, durationType, cardRequired }\`. | +| \`group\` | string | No | Group related plans. Plans in the same group replace each other on upgrade/downgrade. | + +Price intervals: \`"month"\` | \`"quarter"\` | \`"semi_annual"\` | \`"year"\` | \`"one_off"\` + +Trial duration types: \`"day"\` | \`"month"\` | \`"year"\` + +--- + +## Plan Items + +Plan items define what each plan includes -- usage limits, pricing, and billing behavior. + +### \`item(config)\` + +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| \`featureId\` | string | Yes | The \`id\` of the feature to include. | +| \`included\` | number | No | Amount included for free. Omit for boolean features. | +| \`unlimited\` | boolean | No | Grant unlimited usage of this feature. | +| \`reset\` | object | No | How often the included amount resets: \`{ interval, intervalCount? }\`. | +| \`price\` | object | No | Pricing for usage beyond the included amount. | +| \`proration\` | object | No | Mid-cycle changes: \`{ onIncrease, onDecrease }\`. | +| \`rollover\` | object | No | Carry unused balance: \`{ max, expiryDurationType, expiryDurationLength }\`. | + +Reset intervals: \`"hour"\` | \`"day"\` | \`"week"\` | \`"month"\` | \`"quarter"\` | \`"semi_annual"\` | \`"year"\` + +Proration options: +- \`onIncrease\`: \`"prorate"\` | \`"charge_immediately"\` +- \`onDecrease\`: \`"prorate"\` | \`"refund_immediately"\` | \`"no_action"\` + +--- + +## Pricing Patterns + +The \`price\` object on a plan item supports different billing models. + +### Usage-based -- charge based on actual usage \`\`\`typescript -{ feature_id: "api_calls", included_usage: 1000, tiers: [{ to: 5000, amount: 0.02 }, { to: "inf", amount: 0.01 }], usage_model: "pay_per_use", interval: "month" } +item({ + featureId: seats.id, + included: 5, + price: { + amount: 10, + interval: 'month', + billingMethod: 'usage_based', + billingUnits: 1, + }, +}) \`\`\` -Customer gets 1,000 API calls free, then pays $0.02/call up to 5,000, then $0.01/call after that. - -### 6. Per-Unit Pricing Structure - -For any "per-X" pricing (like "$Y per seat", "$Y per project", "$Y per website"), use this pattern: +### Prepaid -- customer buys a fixed quantity upfront \`\`\`typescript -// Base subscription fee -{ feature_id: null, price: 10, interval: "month" } -// Unit allocation -{ feature_id: "seats", included_usage: 1, price: 10, usage_model: "pay_per_use", billing_units: 1 } +item({ + featureId: credits.id, + price: { + amount: 5, + billingUnits: 100, + billingMethod: 'prepaid', + }, +}) \`\`\` -This creates: $10/month base price that includes 1 unit, then $10 per additional unit purchased. +### Tiered -- price changes based on usage volume -**Always** use this two-item pattern for any per-unit pricing - never use pure per-unit without a base fee. +\`\`\`typescript +item({ + featureId: apiCalls.id, + price: { + tiers: [ + { to: 1000, amount: 0.01 }, + { to: 10000, amount: 0.008 }, + { to: 'inf', amount: 0.005 }, + ], + billingMethod: 'usage_based', + interval: 'month', + }, +}) +\`\`\` + +### Price Fields Reference + +| Param | Type | Description | +|-------|------|-------------| +| \`amount\` | number | Price per \`billingUnits\`. Mutually exclusive with \`tiers\`. | +| \`tiers\` | array | Tiered pricing. Each entry: \`{ to: number \\| "inf", amount }\`. Mutually exclusive with \`amount\`. | +| \`billingMethod\` | enum | \`"usage_based"\` \\| \`"prepaid"\`. Required. | +| \`interval\` | enum | \`"week"\` \\| \`"month"\` \\| \`"quarter"\` \\| \`"semi_annual"\` \\| \`"year"\`. Omit for one-time charges. | +| \`billingUnits\` | number | Units per price (default 1). Eg, $5 per 100 credits = \`amount: 5, billingUnits: 100\`. | +| \`maxPurchase\` | number | Maximum quantity that can be purchased. | + +--- + +## Common Patterns + +### Free Plan with Usage Limits + +\`\`\`typescript +export const free = plan({ + id: 'free', + name: 'Free', + autoEnable: true, + items: [ + item({ + featureId: messages.id, + included: 5, + reset: { interval: 'month' }, + }), + item({ + featureId: seats.id, + included: 1, + }), + ], +}); +\`\`\` + +### Paid Plan with Flat Fee + Overage + +\`\`\`typescript +export const pro = plan({ + id: 'pro', + name: 'Pro', + price: { amount: 20, interval: 'month' }, + items: [ + item({ + featureId: messages.id, + included: 1000, + reset: { interval: 'month' }, + price: { + amount: 0.01, + interval: 'month', + billingMethod: 'usage_based', + }, + }), + ], +}); +\`\`\` + +### Per-Unit Pricing (e.g., per seat) + +For any "per-X" pricing (like "$Y per seat"), use a base fee + unit allocation: + +\`\`\`typescript +export const team = plan({ + id: 'team', + name: 'Team', + price: { amount: 10, interval: 'month' }, + items: [ + item({ + featureId: seats.id, + included: 1, + price: { + amount: 10, + interval: 'month', + billingMethod: 'usage_based', + billingUnits: 1, + }, + }), + ], +}); +\`\`\` + +This creates: $10/month base price that includes 1 seat, then $10 per additional seat. + +### Plan with Free Trial + +\`\`\`typescript +export const pro = plan({ + id: 'pro', + name: 'Pro', + price: { amount: 20, interval: 'month' }, + freeTrial: { + durationLength: 14, + durationType: 'day', + cardRequired: true, + }, + items: [ + item({ featureId: messages.id, included: 1000, reset: { interval: 'month' } }), + item({ featureId: sso.id }), + ], +}); +\`\`\` + +### Add-on / Top-up (One-time Prepaid) + +\`\`\`typescript +export const topUp = plan({ + id: 'top_up', + name: 'Message Top-Up', + addOn: true, + items: [ + item({ + featureId: messages.id, + price: { + amount: 5, + billingUnits: 100, + billingMethod: 'prepaid', + }, + }), + ], +}); +\`\`\` + +### Annual Plan Variant + +For annual variants, create a separate plan with annual price interval: + +\`\`\`typescript +export const proAnnual = plan({ + id: 'pro_annual', + name: 'Pro - Annual', + group: 'pro', + price: { amount: 192, interval: 'year' }, + items: [ + item({ featureId: messages.id, included: 1000, reset: { interval: 'month' } }), + ], +}); +\`\`\` + +--- + +## Full Example + +A complete config with a free plan, a paid plan with a trial, and a credits top-up add-on: + +\`\`\`typescript +// autumn.config.ts +import { feature, item, plan } from 'atmn'; + +// Features +export const messages = feature({ + id: 'messages', + name: 'Messages', + type: 'metered', + consumable: true, +}); + +export const seats = feature({ + id: 'seats', + name: 'Seats', + type: 'metered', + consumable: false, +}); + +export const sso = feature({ + id: 'sso', + name: 'SSO', + type: 'boolean', +}); + +// Plans +export const free = plan({ + id: 'free', + name: 'Free', + autoEnable: true, + items: [ + item({ + featureId: messages.id, + included: 5, + reset: { interval: 'month' }, + }), + item({ + featureId: seats.id, + included: 1, + }), + ], +}); + +export const pro = plan({ + id: 'pro', + name: 'Pro', + price: { amount: 20, interval: 'month' }, + freeTrial: { + durationLength: 14, + durationType: 'day', + cardRequired: true, + }, + items: [ + item({ + featureId: messages.id, + included: 1000, + reset: { interval: 'month' }, + }), + item({ + featureId: seats.id, + included: 5, + price: { + amount: 10, + interval: 'month', + billingMethod: 'usage_based', + billingUnits: 1, + }, + }), + item({ + featureId: sso.id, + }), + ], +}); + +export const topUp = plan({ + id: 'top_up', + name: 'Message Top-Up', + addOn: true, + items: [ + item({ + featureId: messages.id, + price: { + amount: 5, + billingUnits: 100, + billingMethod: 'prepaid', + }, + }), + ], +}); +\`\`\` + +--- ## Guidelines +### Start Simple + +- If the user describes more than 3 features, start with the 3 most important (prioritize metered features) and ask them to confirm before adding more +- Inform them you kept it simple to start with, but they can add more later + +### Disambiguate Pricing Model + +- When the user mentions a price for a feature, ask whether it should be **usage-based** (pay as you go, billed at the end of the cycle) or **prepaid** (buy a fixed quantity upfront) +- Don't assume one or the other without asking + +### Don't Fabricate Capabilities + +- If the user asks about pricing or functionality you're not sure Autumn supports, do NOT make it up or assume it can be done +- Point them to Discord (https://discord.gg/atmn) or docs (https://docs.useautumn.com/llms.txt) instead + ### Naming Conventions -- Product and Feature IDs should be lowercase with underscores (e.g., \`pro_plan\`, \`chat_messages\`) +- Feature and plan IDs should be lowercase with underscores (e.g., \`pro_plan\`, \`chat_messages\`) -### Features vs Plan Features +### Features vs Plan Items - Features define WHAT can be tracked (e.g., "credits") -- Plan features define HOW a feature is granted in a plan (recurring, one-time, free, paid) +- Plan items define HOW a feature is granted in a plan (recurring, one-time, free, paid) - Never create duplicate features for the same underlying resource - Example: "monthly tokens" and "one-time tokens" should be the SAME feature ("tokens"), referenced by different plan items with different intervals ### Default Plans -- **Never** set \`is_default: true\` for plans with prices +- **Never** set \`autoEnable: true\` for plans with prices - Default plans must be free ### Enterprise Plans @@ -116,85 +480,18 @@ This creates: $10/month base price that includes 1 unit, then $10 per additional - Ignore "Enterprise" plans with custom pricing in the config - Custom plans can be created per-customer in the Autumn dashboard -### Annual Plans - -- For annual variants, create a separate plan with annual price interval -- Name it \` - Annual\` - ### Currency - Currency can be changed in the Autumn dashboard under Developer > Stripe -## Example Configuration +## Previewing and Pushing Changes -\`\`\`typescript -import { feature, plan, item } from "atmn"; +After updating \`autumn.config.ts\`: -// Features -export const messages = feature({ - id: "messages", - name: "Messages", - type: "metered", - consumable: true, -}); - -export const seats = feature({ - id: "seats", - name: "Team Seats", - type: "metered", - consumable: false, -}); - -// Plans -export const free = plan({ - id: "free", - name: "Free", - autoEnable: true, - items: [ - item({ featureId: messages.id, included: 100 }), - item({ featureId: seats.id, included: 1 }), - ], -}); - -export const pro = plan({ - id: "pro", - name: "Pro", - price: { - amount: 29, - interval: "month", - }, - items: [ - item({ - featureId: seats.id, - included: 5, - price: { - amount: 10, - interval: "month", - billingMethod: "usage_based", - }, - }), - item({ - featureId: messages.id, - included: 10_000, - entityFeatureId: seats.id, - price: { - amount: 0.01, - interval: "month", - billingMethod: "usage_based", - }, - }), - ], -}); -\`\`\` - -## Next Steps - -Once you've designed your pricing: - -1. Update \`autumn.config.ts\` with your features and plans -2. Run \`atmn preview\` to lint, validate and preview your plans - make sure to show the user the output to ensure they're happy with the results. -3. Run \`atmn push\` to sync your configuration to Autumn -4. Test in sandbox mode before going live +1. **Preview first**: Run \`atmn preview\` to lint, validate and preview your plans. Show the output to the user so they can review the full configuration. +2. **Get confirmation**: Ask the user to review, edit, and confirm the preview output before pushing. Do NOT push until the user explicitly confirms. +3. **Push**: Once the user is happy, run \`atmn push\` to sync the configuration to Autumn. +4. Test in sandbox mode before going live. You can push to production with \`atmn push -p\`. ## Resources diff --git a/packages/atmn/src/prompts/skills/autumn-setup.ts b/packages/atmn/src/prompts/skills/autumn-setup.ts new file mode 100644 index 000000000..5796eac39 --- /dev/null +++ b/packages/atmn/src/prompts/skills/autumn-setup.ts @@ -0,0 +1,340 @@ +export default `--- +name: autumn-setup +description: | + Sets up Autumn billing integration: installs the SDK, creates a customer, and adds the payment flow. + Use this skill when the user wants to: + - Set up Autumn billing + - Create an Autumn customer + - Integrate Autumn into their app + - Add billing/entitlements with Autumn + - Configure Autumn SDK + - Add payment flow or checkout +--- + +# Set up Autumn Billing + +Autumn is a billing and entitlements layer over Stripe. This skill walks through installing the SDK, creating an Autumn customer, and wiring up the payment flow. + +> **Before starting:** Check for an \`autumn.config.ts\` in the project root. If it doesn't exist, run \`npx atmn init\` to log in and generate the file (this saves your API key and syncs your config). Then refer to \`autumn.config.ts\` for your product and feature IDs. + +## Step 1: Analyze the Codebase + +Before making changes, detect: + +- **Language**: TypeScript/JavaScript, Python, or other +- **If TS/JS - Framework**: Next.js, Hono, or other +- **If TS/JS - React frontend?**: Check for React in package.json +- **Customer model**: Look at the auth setup to determine whether customers map to individual users or organizations. Check for org/team/workspace models in the codebase. + +If it's clear from the codebase (e.g., there's an org model and team-based auth), state your assumption. If it's ambiguous, ask the user: + +> **Should Autumn customers be individual users, or organizations?** +> - **Users (B2C)**: Each user has their own plan and limits +> - **Organizations (B2B)**: Plans and limits are shared across an org + +## Step 2: Create a Plan and Confirm + +Before writing any integration code, present a short plan to the user covering: + +- What stack/framework you detected +- Whether customers are users or orgs (and why you think so) +- Which path you're following (React fullstack vs backend-only) +- Which files you'll create or modify +- Where the handler / provider / customer creation will go +- Where the payment flow will be wired up + +Ask the user to **read, edit, and confirm** the plan before proceeding. Do NOT start coding until the user approves. + +--- + +## Path A: React + Node.js (Fullstack TypeScript) + +Use this path if there's a React frontend with a Node.js backend. + +### A1. Install the SDK + +Use the package manager already installed (npm, yarn, pnpm, bun): + +\`\`\`bash +npm install autumn-js +\`\`\` + +### A2. Mount the Handler (Server-Side) + +This creates endpoints at \`/api/autumn/*\` that the React hooks will call. The \`identify\` function should return either the user ID or org ID from your auth provider, depending on how you're using Autumn. + +#### Next.js (App Router) + +\`\`\`typescript +// app/api/autumn/[...all]/route.ts +import { autumnHandler } from "autumn-js/next"; + +export const { GET, POST } = autumnHandler({ + identify: async (request) => { + // Get user/org from your auth provider + const session = await auth.api.getSession({ headers: request.headers }); + return { + customerId: session?.user.id, // or session?.org.id for B2B + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +\`\`\` + +#### Hono + +\`\`\`typescript +import { autumnHandler } from "autumn-js/hono"; + +app.use("/api/autumn/*", autumnHandler({ + identify: async (c) => { + const session = await auth.api.getSession({ headers: c.req.raw.headers }); + return { + customerId: session?.user.id, // or session?.org.id for B2B + customerData: { name: session?.user.name, email: session?.user.email }, + }; + }, +})); +\`\`\` + +#### Other Frameworks (Generic Handler) + +For any framework not listed above, use the generic handler: + +\`\`\`typescript +import { autumnHandler } from "autumn-js/backend"; + +// Mount this handler onto the /api/autumn/* path in your backend +const handleRequest = async (request) => { + const session = await auth.api.getSession({ headers: request.headers }); + + let body = null; + if (request.method !== "GET") { + body = await request.json(); + } + + const { statusCode, response } = await autumnHandler({ + customerId: session?.user.id, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + request: { + url: request.url, + method: request.method, + body: body, + }, + }); + + return new Response(JSON.stringify(response), { + status: statusCode, + headers: { "Content-Type": "application/json" }, + }); +}; +\`\`\` + +### A3. Add the Provider (Client-Side) + +Wrap your app with \`AutumnProvider\`: + +\`\`\`tsx +import { AutumnProvider } from "autumn-js/react"; + +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} +\`\`\` + +If your backend is on a different URL (e.g., Vite + separate server), pass \`backendUrl\`: + +\`\`\`tsx + +\`\`\` + +### A4. Create a Customer + +Add this hook to any component. It automatically creates an Autumn customer for new users and fetches existing customer state: + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +const { data } = useCustomer(); +console.log("Autumn customer:", data); +\`\`\` + +Autumn's customer ID is the same as your internal user or org ID from your auth provider. No need to store any extra IDs. + +### A5. Stripe Payment Flow + +Call \`attach\` when the customer wants to purchase a plan. This returns a Stripe payment URL. Once they pay, Autumn grants access to the features defined in the plan. + +\`\`\`tsx +import { useCustomer } from "autumn-js/react"; + +export default function PurchaseButton() { + const { attach } = useCustomer(); + + return ( + + ); +} +\`\`\` + +This handles all plan change scenarios (upgrades, downgrades, one-time topups, renewals, etc). + +The \`redirectMode: "always"\` flag always returns a payment URL: +- New purchases redirect to Stripe Checkout to enter payment details +- Subsequent charges redirect to an Autumn hosted, one-click confirmation page + +--- + +## Path B: Backend Only (Node.js, Python, or Other) + +Use this path if there's no React frontend, or you prefer server-side only. + +### B1. Install the SDK + +**Node.js:** + +\`\`\`bash +npm install autumn-js +\`\`\` + +**Python:** + +\`\`\`bash +pip install autumn-sdk +\`\`\` + +### B2. Initialize the Client + +**TypeScript/JavaScript:** + +\`\`\`typescript +import { Autumn } from "autumn-js"; + +const autumn = new Autumn({ + secretKey: process.env.AUTUMN_SECRET_KEY, +}); +\`\`\` + +**Python:** + +\`\`\`python +from autumn_sdk import Autumn + +autumn = Autumn('am_sk_test_xxx') +\`\`\` + +### B3. Create a Customer + +When the customer signs up, create an Autumn customer. Autumn will automatically enable any \`autoEnable\` plan (typically Free). + +**TypeScript:** + +\`\`\`typescript +const customer = await autumn.customers.getOrCreate({ + customerId: "user_or_org_id_from_auth", + name: "John Doe", + email: "john@example.com", +}); +\`\`\` + +**Python:** + +\`\`\`python +customer = await autumn.customers.get_or_create( + customer_id="user_or_org_id_from_auth", + name="John Doe", + email="john@example.com", +) +\`\`\` + +**cURL:** + +\`\`\`bash +curl -X POST https://api.useautumn.com/v1/customers \\ + -H "Authorization: Bearer am_sk_test_xxx" \\ + -H "Content-Type: application/json" \\ + -d '{"customer_id": "user_or_org_id_from_auth", "name": "John Doe", "email": "john@example.com"}' +\`\`\` + +Autumn's customer ID is the same as your internal user or org ID from your auth provider. No need to store any extra IDs. + +### B4. Stripe Payment Flow + +Call \`attach\` when the customer wants to purchase a plan. This returns a Stripe payment URL. Redirect the customer to complete payment. + +**TypeScript:** + +\`\`\`typescript +const response = await autumn.billing.attach({ + customerId: "user_or_org_id_from_auth", + planId: "pro", + redirectMode: "always", +}); + +redirect(response.paymentUrl); +\`\`\` + +**Python:** + +\`\`\`python +response = await autumn.billing.attach( + customer_id="user_or_org_id_from_auth", + plan_id="pro", + redirect_mode="always", +) +# Redirect to response.payment_url +\`\`\` + +**cURL:** + +\`\`\`bash +curl -X POST 'https://api.useautumn.com/v1/attach' \\ + -H 'Authorization: Bearer am_sk_test_xxx' \\ + -H 'Content-Type: application/json' \\ + -d '{ + "customer_id": "user_or_org_id_from_auth", + "plan_id": "pro", + "redirect_mode": "always" + }' +\`\`\` + +This handles all plan change scenarios (upgrades, downgrades, one-time topups, renewals, etc). + +The \`redirectMode: "always"\` flag always returns a payment URL: +- New purchases redirect to Stripe Checkout to enter payment details +- Subsequent charges redirect to an Autumn hosted, one-click confirmation page + +--- + +## Verification + +After setup, report to the user: + +1. What stack you detected +2. Which path you followed +3. What files you created/modified +4. That the Autumn customer is logged in browser, and to check in the Autumn dashboard + +**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root. + +**Documentation:** https://docs.useautumn.com/llms.txt +`; diff --git a/packages/atmn/src/prompts/skills/index.ts b/packages/atmn/src/prompts/skills/index.ts index 47a98aa4b..45c61f375 100644 --- a/packages/atmn/src/prompts/skills/index.ts +++ b/packages/atmn/src/prompts/skills/index.ts @@ -1,10 +1,10 @@ // Skills are YAML-frontmatter markdown files that follow the SKILLS standard // for AI coding assistants (Claude, Cursor, OpenCode, etc.) -import autumnCreatingCustomersContent from "./autumn-creating-customers.js"; -import autumnAcceptingPaymentsContent from "./autumn-accepting-payments.js"; +import autumnBillingPageContent from "./autumn-billing-page.js"; +import autumnGatingContent from "./autumn-gating.js"; import autumnModellingPricingPlansContent from "./autumn-modelling-pricing-plans.js"; -import autumnTrackingMeteredUsageContent from "./autumn-tracking-metered-usage.js"; +import autumnSetupContent from "./autumn-setup.js"; export interface Skill { id: string; @@ -15,16 +15,22 @@ export interface Skill { export const skills: Skill[] = [ { - id: "autumn-creating-customers", - name: "Creating Customers", - description: "Set up Autumn billing integration", - content: autumnCreatingCustomersContent, + id: "autumn-setup", + name: "Setup and Payments", + description: "Install SDK, create customers, and add payment flow", + content: autumnSetupContent, }, { - id: "autumn-accepting-payments", - name: "Accepting Payments", - description: "Add checkout, plan changes, and billing UI", - content: autumnAcceptingPaymentsContent, + id: "autumn-gating", + name: "Checking and Tracking", + description: "Add usage tracking and feature gating", + content: autumnGatingContent, + }, + { + id: "autumn-billing-page", + name: "Build Your Billing Page", + description: "Display billing state, plan switching, and subscriptions", + content: autumnBillingPageContent, }, { id: "autumn-modelling-pricing-plans", @@ -32,12 +38,11 @@ export const skills: Skill[] = [ description: "Design pricing models with autumn.config.ts", content: autumnModellingPricingPlansContent, }, - { - id: "autumn-tracking-metered-usage", - name: "Tracking Metered Usage", - description: "Add usage tracking and feature gating", - content: autumnTrackingMeteredUsageContent, - }, ]; -export { autumnCreatingCustomersContent, autumnAcceptingPaymentsContent, autumnModellingPricingPlansContent, autumnTrackingMeteredUsageContent }; +export { + autumnSetupContent, + autumnGatingContent, + autumnBillingPageContent, + autumnModellingPricingPlansContent, +}; diff --git a/packages/atmn/src/prompts/usage.ts b/packages/atmn/src/prompts/usage.ts deleted file mode 100644 index 952542d56..000000000 --- a/packages/atmn/src/prompts/usage.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { creditSystemDocs } from "./creditSystemDocs.js"; - -export const usagePrompt = `## Add Autumn gating and usage tracking - -Autumn tracks feature usage and enforces limits. Add usage tracking to this codebase. - -### Step 1: Detect my integration type - -Check if this codebase already has Autumn set up: -- If there's an \`AutumnProvider\` and \`autumnHandler\` mounted → **React hooks available** (can use for UX) -- Backend SDK should **always** be used to enforce limits server-side - -Tell me what you detected before proceeding. - ---- - -## Frontend checks (React hooks) - -Use frontend checks for **UX only** - showing/hiding features, prompting upgrades. These should NOT be trusted for security. - -### Check feature access -\`\`\`tsx -import { useCustomer } from "autumn-js/react"; - -export function SendChatMessage() { - const { check, refetch } = useCustomer(); - - const handleSendMessage = async () => { - const { data } = check({ featureId: "messages" }); - - if (!data?.allowed) { - alert("You're out of messages"); - } else { - //send chatbot message - //then, refresh customer usage data - await refetch(); - } - }; -} -\`\`\` - ---- - -## Backend checks (required for security) - -**Always check on the backend** before executing any protected action. Frontend checks can be bypassed. - -### TypeScript -\`\`\`typescript -import { Autumn } from "autumn-js"; - -const autumn = new Autumn({ - secretKey: process.env.AUTUMN_SECRET_KEY, -}); - -// Check before executing the action -const { data } = await autumn.check({ - customer_id: "user_or_org_id_from_auth", - feature_id: "api_calls", -}); - -if (!data.allowed) { - return { error: "Usage limit reached" }; -} - -// Safe to proceed - do the actual work here -const result = await doTheActualWork(); - -// Track usage after success -await autumn.track({ - customer_id: "user_or_org_id_from_auth", - feature_id: "api_calls", - value: 1, -}); - -return result; -\`\`\` - -### Python -\`\`\`python -from autumn import Autumn - -autumn = Autumn('am_sk_test_xxx') - -# Check before executing the action -response = await autumn.check( - customer_id="user_or_org_id_from_auth", - feature_id="api_calls" -) - -if not response.allowed: - raise HTTPException(status_code=403, detail="Usage limit reached") - -# Safe to proceed - do the actual work here -result = await do_the_actual_work() - -# Track usage after success -await autumn.track( - customer_id="user_or_org_id_from_auth", - feature_id="api_calls", - value=1 -) - -return result -\`\`\` - ---- - -## Notes - -- **Frontend checks** = UX (show/hide UI, display limits) - can be bypassed by users -- **Backend checks** = Security (enforce limits) - required before any protected action -- Pattern: check → do work → track (only track after successful completion) -- Feature IDs come from the Autumn configuration -- Current usage and total limit can be taken from from Customer object and displayed -- see the Customer types from the Autumn SDK -\`\`\`tsx -import type { Customer } from "autumn-js"; - -//Balance is: customer.features..balance -\`\`\` - -For credit systems, see the end of this message. - -**Note:** Your Autumn configuration is in \`autumn.config.ts\` in your project root. - -Docs: https://docs.useautumn.com/llms.txt - - -${creditSystemDocs} - -`; diff --git a/packages/atmn/src/views/react/init/steps/HandoffStep.tsx b/packages/atmn/src/views/react/init/steps/HandoffStep.tsx index ce6e3075b..cf7760895 100644 --- a/packages/atmn/src/views/react/init/steps/HandoffStep.tsx +++ b/packages/atmn/src/views/react/init/steps/HandoffStep.tsx @@ -13,9 +13,9 @@ If this file is empty, then you should help the user model their pricing structu Once a pricing model is either decided upon or already found to exist already continue onwards: -- Begin by helping the user create their first customer in Autumn by loading the 'autumn-creating-customer' skill. -- Then setup accepting payments by loading the 'autumn-accepting-payments' skill. -- Lastly start tracking usage by loading the 'autumn-tracking-usage' skill.`; +- Begin by setting up Autumn in the codebase by loading the 'autumn-setup' skill (install SDK, create customer, payment flow). +- Then build the billing page by loading the 'autumn-billing-page' skill (display plans, switching, cancelling). +- Lastly add usage gating by loading the 'autumn-gating' skill (check access, track usage).`; interface HandoffStepProps { step: number; @@ -193,7 +193,10 @@ export function HandoffStep({ and implement Autumn into your codebase? - handleAiChoice(item.value)} /> + handleAiChoice(item.value)} + /> @@ -229,12 +232,12 @@ export function HandoffStep({ Enter the custom path (relative to project root): {">"} - + @@ -284,10 +287,10 @@ export function HandoffStep({ )} - handleNextStepsChoice(item.value)} - /> + handleNextStepsChoice(item.value)} + /> diff --git a/vite/package.json b/vite/package.json index 7912590fa..b5c4cf42b 100644 --- a/vite/package.json +++ b/vite/package.json @@ -19,6 +19,7 @@ "dependencies": { "@ai-sdk/react": "^3.0.25", "@autumn/shared": "workspace:*", + "atmn": "workspace:*", "@better-auth/dash": "catalog:", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", diff --git a/vite/src/components/v2/CodeGroup.tsx b/vite/src/components/v2/CodeGroup.tsx index 8c50beb02..39397fba7 100644 --- a/vite/src/components/v2/CodeGroup.tsx +++ b/vite/src/components/v2/CodeGroup.tsx @@ -109,7 +109,7 @@ interface CodeGroupContentProps const CodeGroupContent = React.forwardRef< React.ElementRef, CodeGroupContentProps ->(({ className, children, ...props }, ref) => { +>(({ className, children, copyText: _copyText, ...props }, ref) => { return ( - +
{features.length === 0 ? (
diff --git a/vite/src/components/v2/SDKSelector.tsx b/vite/src/components/v2/SDKSelector.tsx index 02785a22d..47bf485f2 100644 --- a/vite/src/components/v2/SDKSelector.tsx +++ b/vite/src/components/v2/SDKSelector.tsx @@ -11,17 +11,28 @@ import { interface SDKSelectorProps { className?: string; + excludeSDKs?: SDKType[]; } -export function SDKSelector({ className }: SDKSelectorProps) { +export function SDKSelector({ className, excludeSDKs }: SDKSelectorProps) { const selectedSDK = useSDKStore((s) => s.selectedSDK); const setSelectedSDK = useSDKStore((s) => s.setSelectedSDK); - const selectedOption = SDK_OPTIONS.find((opt) => opt.value === selectedSDK); + const visibleOptions = excludeSDKs + ? SDK_OPTIONS.filter((opt) => !excludeSDKs.includes(opt.value)) + : SDK_OPTIONS; + + const effectiveSDK = excludeSDKs?.includes(selectedSDK) + ? "node" + : selectedSDK; + + const selectedOption = visibleOptions.find( + (opt) => opt.value === effectiveSDK, + ); return (