AgentSkillsCN

implementing-autumn-billing

在应用程序中实现 Autumn 计费与支付功能。适用于构建订阅系统、按使用量计费、付费墙、功能门控,或各类商业化功能时使用。Autumn 是一款开源计费层,位于你的应用与 Stripe 之间。

SKILL.md
--- frontmatter
name: implementing-autumn-billing
description: Implement Autumn billing and payments in applications. Use when building subscription systems, usage-based billing, paywalls, feature gates, or monetization features. Autumn is an open-source billing layer between your app and Stripe.

Implementing Autumn Billing

Autumn is an open-source billing management system that sits between your application and Stripe. It handles subscription state, usage metering, credit balances, and feature gating without requiring webhook management.

When to Use This Skill

  • Adding subscription/payment features to an app
  • Implementing usage-based billing (metered features)
  • Creating paywalls or feature gates
  • Building credit systems
  • Managing subscription tiers (Free/Pro/Enterprise)
  • Implementing per-seat or per-entity pricing

Installation

For Convex Projects

bash
npm install @useautumn/convex autumn-js
# or
bun add @useautumn/convex autumn-js

For Next.js / Express Projects

bash
npm install autumn-js
# or
bun add autumn-js

Environment Setup

Add to your .env or environment variables:

bash
AUTUMN_SECRET_KEY=am_sk_your_secret_key

Get your secret key from https://app.useautumn.com

Pricing as Code (CLI)

Autumn supports defining features and products in code using autumn.config.ts:

1. Initialize Project

bash
npx atmn init

2. Define Features and Products

Create autumn.config.ts:

typescript
import {
  feature,
  product,
  featureItem,
  priceItem,
} from "atmn";

// Features
const messages = feature({
  id: "messages",
  name: "Messages",
  type: "single_use", // Consumed on each use
});

const workspaces = feature({
  id: "workspaces",
  name: "Workspaces",
  type: "single_use",
});

// Products
const free = product({
  id: "free",
  name: "Free",
  is_default: true,
  items: [
    featureItem({
      feature_id: messages.id,
      included_usage: 50,
      interval: "month", // Resets monthly
    }),
    featureItem({
      feature_id: workspaces.id,
      included_usage: 2,
    }),
  ],
});

const pro = product({
  id: "pro",
  name: "Pro",
  items: [
    priceItem({
      price: 19,
      interval: "month",
    }),
    featureItem({
      feature_id: messages.id,
      included_usage: 500,
      interval: "month",
    }),
    featureItem({
      feature_id: workspaces.id,
      included_usage: 10,
    }),
  ],
});

const enterprise = product({
  id: "enterprise",
  name: "Enterprise",
  items: [
    featureItem({
      feature_id: messages.id,
      unlimited: true,
    }),
    featureItem({
      feature_id: workspaces.id,
      unlimited: true,
    }),
  ],
});

export default {
  features: [messages, workspaces],
  products: [free, pro, enterprise],
};

3. Push to Autumn

bash
npx atmn push

CLI Commands

CommandDescription
npx atmn initInitialize project, pull existing config
npx atmn pushPush local config to Autumn
npx atmn pullPull remote config to local
npx atmn authAuthenticate with Autumn
npx atmn dashboardOpen Autumn dashboard

Backend Integration

Convex Setup

  1. Register the component in convex/convex.config.ts:
typescript
import { defineApp } from "convex/server";
import autumn from "@useautumn/convex/convex.config";

const app = defineApp();
app.use(autumn);

export default app;
  1. Initialize the client in convex/autumn.ts:
typescript
import { components } from "./_generated/api";
import { Autumn } from "@useautumn/convex";

export const autumn = new Autumn(components.autumn, {
  secretKey: process.env.AUTUMN_SECRET_KEY ?? "",
  identify: async (ctx: any) => {
    // Customize based on your auth provider
    const user = await ctx.auth.getUserIdentity();
    if (!user) return null;

    return {
      customerId: user.subject, // Unique user ID
      customerData: {
        name: user.name as string,
        email: user.email as string,
      },
    };
  },
});

// Export API functions
export const {
  track,
  cancel,
  query,
  attach,
  check,
  checkout,
  usage,
  setupPayment,
  createCustomer,
  listProducts,
  billingPortal,
  createReferralCode,
  redeemReferralCode,
  createEntity,
  getEntity,
} = autumn.api();

Next.js / Express Setup

typescript
// lib/autumn.ts
import { Autumn } from "autumn-js";

export const autumn = new Autumn({
  secretKey: process.env.AUTUMN_SECRET_KEY!,
});

// In API routes
export async function POST(req: Request) {
  const { userId } = await getAuth(req);

  const result = await autumn.check({
    customerId: userId,
    featureId: "api_calls",
  });

  if (!result.allowed) {
    return Response.json({ error: "Limit exceeded" }, { status: 403 });
  }

  // Process request...

  await autumn.track({
    customerId: userId,
    featureId: "api_calls",
    value: 1,
  });

  return Response.json({ success: true });
}

Core API Reference

Check Feature Access

Check if a customer can access a feature:

typescript
// Convex
const result = await autumn.check(ctx, {
  featureId: "messages",
  requiredBalance: 1, // Optional: minimum balance needed
});

if (result.allowed) {
  // Allow action
} else {
  // Show upgrade prompt
}

// Direct API
const response = await fetch("https://api.useautumn.com/v1/check", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${AUTUMN_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    customer_id: userId,
    feature_id: "messages",
    required_balance: 1,
  }),
});

Response:

json
{
  "allowed": true,
  "customer_id": "user_123",
  "balance": {
    "feature_id": "messages",
    "unlimited": false,
    "granted_balance": 100,
    "purchased_balance": 0,
    "current_balance": 75,
    "usage": 25,
    "overage_allowed": false,
    "reset": {
      "interval": "month",
      "interval_count": 1,
      "resets_at": 1735689600000
    },
    "plan_id": "pro"
  }
}

Track Usage

Record usage for metered features:

typescript
// Convex
await autumn.track(ctx, {
  featureId: "api_calls",
  value: 1,
});

// Direct API
await fetch("https://api.useautumn.com/v1/track", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${AUTUMN_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    customer_id: userId,
    feature_id: "api_calls",
    value: 1,
    idempotency_key: "unique_request_id", // Optional: prevent duplicate tracking
  }),
});

Initiate Checkout

Start a checkout session for upgrades:

typescript
// Convex
const result = await autumn.checkout(ctx, {
  productId: "pro",
});
// Returns { url: "https://checkout.stripe.com/..." }

// Direct API
const response = await fetch("https://api.useautumn.com/v1/checkout", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${AUTUMN_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    customer_id: userId,
    product_id: "pro",
    success_url: "https://yourapp.com/success",
    cancel_url: "https://yourapp.com/pricing",
  }),
});

Billing Portal

Open Stripe billing portal for subscription management:

typescript
const result = await autumn.billingPortal(ctx, {});
// Returns { url: "https://billing.stripe.com/..." }

Frontend Integration

React Provider (Web)

tsx
// providers/autumn-provider.tsx
"use client";
import { AutumnProvider } from "autumn-js/react";
import { api } from "../convex/_generated/api";
import { useConvex } from "convex/react";

export function AutumnWrapper({ children }: { children: React.ReactNode }) {
  const convex = useConvex();

  return (
    <AutumnProvider convex={convex} convexApi={(api as any).autumn}>
      {children}
    </AutumnProvider>
  );
}

Using the Hook

tsx
import { useCustomer, CheckoutDialog } from "autumn-js/react";

function UpgradeButton() {
  const { customer, checkout, check } = useCustomer();

  const handleUpgrade = async () => {
    await checkout({
      productId: "pro",
      dialog: CheckoutDialog, // Optional: built-in dialog
    });
  };

  return <button onClick={handleUpgrade}>Upgrade to Pro</button>;
}

Pricing Table Component

tsx
import { PricingTable } from "autumn-js/react";

function PricingPage() {
  return <PricingTable />;
}

Autumn Dashboard Configuration

1. Create Features

In the Autumn dashboard, define your metered features:

Feature IDTypeReset Interval
messagesMeteredMonthly
workspacesMeteredNone (cumulative)
api_callsMeteredMonthly
storage_gbMeteredNone

2. Create Products/Plans

Define your pricing tiers:

Free Plan:

  • messages: 50/month
  • workspaces: 2

Pro Plan ($19/mo):

  • messages: 500/month
  • workspaces: 10

Enterprise (Custom):

  • messages: Unlimited
  • workspaces: Unlimited

3. Connect Stripe

Link your Stripe account in the Autumn dashboard to enable payment processing.

Common Patterns

Paywall Before Action

typescript
async function sendMessage(ctx, args) {
  // 1. Check entitlement
  const result = await autumn.check(ctx, {
    featureId: "messages",
  });

  if (!result.allowed) {
    throw new Error("BILLING_LIMIT_EXCEEDED");
  }

  // 2. Perform action
  const message = await createMessage(args);

  // 3. Track usage
  await autumn.track(ctx, {
    featureId: "messages",
    value: 1,
  });

  return message;
}

Entity-Based Billing (Per-Seat)

typescript
// Track usage per entity (e.g., per team member)
await autumn.track(ctx, {
  featureId: "seats",
  entityId: teamMemberId,
  value: 1,
  entityData: {
    name: memberName,
    email: memberEmail,
  },
});

// Check entity-specific access
const result = await autumn.check(ctx, {
  featureId: "seats",
  entityId: teamMemberId,
});

Credit System

typescript
// Grant credits
await autumn.attach(ctx, {
  featureId: "credits",
  value: 100,
});

// Use credits
await autumn.track(ctx, {
  featureId: "credits",
  value: 10,
});

// Check remaining
const result = await autumn.check(ctx, {
  featureId: "credits",
});
console.log(result.balance.current_balance); // 90

Handling Billing Errors in UI

tsx
function ChatInput() {
  const [error, setError] = useState(null);
  const [showUpgrade, setShowUpgrade] = useState(false);

  const handleSend = async (message) => {
    try {
      await sendMessage({ content: message });
    } catch (err) {
      if (err.message.includes("BILLING_LIMIT_EXCEEDED")) {
        setShowUpgrade(true);
      } else {
        setError(err.message);
      }
    }
  };

  return (
    <>
      <Input onSubmit={handleSend} />
      {showUpgrade && <UpgradeModal onClose={() => setShowUpgrade(false)} />}
    </>
  );
}

Error Handling Best Practices

  1. Fail open on API errors: If Autumn API is unavailable, allow the action rather than blocking users.
typescript
try {
  const result = await autumn.check(ctx, { featureId: "messages" });
  if (!result.allowed) {
    throw new Error("BILLING_LIMIT_EXCEEDED");
  }
} catch (error) {
  if (error.message === "BILLING_LIMIT_EXCEEDED") {
    throw error; // Re-throw billing errors
  }
  // Log but continue on API errors
  console.error("[Autumn] Check failed:", error);
}
  1. Use idempotency keys for tracking to prevent duplicate charges.

  2. Track after success, not before, to avoid charging for failed operations.

Testing

Test Mode

Use test API keys (prefix am_sk_test_) for development:

bash
AUTUMN_SECRET_KEY=am_sk_test_xxx

Mock Responses

For unit tests, mock the Autumn API responses:

typescript
jest.mock("autumn-js", () => ({
  check: jest.fn().mockResolvedValue({ allowed: true, balance: { current_balance: 50 } }),
  track: jest.fn().mockResolvedValue({ success: true }),
}));

Resources