Add blog section, update landing page components, and fix mobile pricing header

Made-with: Cursor
This commit is contained in:
Ayush Rodrigues
2026-04-15 20:50:22 +01:00
parent a2046f1ac1
commit f6c5046c32
37 changed files with 1621 additions and 11 deletions

View File

@@ -0,0 +1,108 @@
import { getAllPosts, getPostBySlug } from "@/lib/blogUtils";
import { mdxComponents } from "@/components/blogComponents";
import { MDXRemote } from "next-mdx-remote/rsc";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
export function generateStaticParams() {
return getAllPosts().map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = getPostBySlug({ slug });
if (!post) return { title: "Post Not Found" };
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: "article",
publishedTime: post.date,
authors: [post.author],
...(post.image && {
images: [{ url: post.image }],
}),
},
};
}
function formatDate(dateString) {
if (!dateString) return "";
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
export default async function BlogPostPage({ params }) {
const { slug } = await params;
const post = getPostBySlug({ slug });
if (!post) notFound();
return (
<div className="py-16 md:py-24 bg-[#0F0F0F]">
<div className="max-w-[720px] mx-auto px-4 xl:px-0">
<Link
href="/blog"
className="inline-flex items-center gap-2 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66] hover:text-white transition-colors duration-300 mb-10"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
className="rotate-180"
>
<path
d="M6 3L11 8L6 13"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Back to blog
</Link>
<header className="mb-12">
<div className="flex items-center gap-3 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66] mb-4">
<span>{formatDate(post.date)}</span>
<span className="w-1 h-1 bg-[#FFFFFF44] rounded-full" />
<span>{post.author}</span>
</div>
<h1 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1] font-sans text-white mb-4">
{post.title}
</h1>
{post.description && (
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans">
{post.description}
</p>
)}
</header>
{post.image && (
<div className="relative w-full aspect-[2/1] overflow-hidden border border-[#292929] mb-12">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover"
priority
/>
</div>
)}
<hr className="border-[#292929] mb-12" />
<article className="prose prose-invert prose-lg max-w-none">
<MDXRemote source={post.source} components={mdxComponents} />
</article>
</div>
</div>
);
}

View File

@@ -0,0 +1,30 @@
import Navbar from "@/components/navbar";
import Footer from "@/components/footer";
export default function BlogLayout({ children }) {
return (
<div
className="w-full overflow-x-clip"
style={{ "--page-pad": "max(2.5rem, calc((100vw - 1440px) / 2))" }}
>
<div className="relative z-10 bg-[#0F0F0F] min-h-screen">
<div className="relative w-full px-4 md:px-(--page-pad) pt-5">
<div className="absolute pointer-events-none top-0 bottom-0 left-4 md:left-(--page-pad) border-l border-[#292929] z-50" />
<Navbar />
<div className="absolute pointer-events-none top-0 bottom-0 right-4 md:right-(--page-pad) border-r border-[#292929] z-50" />
<div className="flex flex-col gap-2.5 mt-2.5">
<div className="border-t border-[#292929] w-full" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
<div className="border-t border-[#292929] w-full hidden md:block" />
</div>
{children}
<Footer />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
import { getAllPosts } from "@/lib/blogUtils";
import Image from "next/image";
import Link from "next/link";
export const metadata = {
title: "Blog",
description:
"Thoughts on billing infrastructure, usage-based pricing, and building for AI startups.",
};
function formatDate(dateString) {
if (!dateString) return "";
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
}
export default function BlogListingPage() {
const posts = getAllPosts();
return (
<div className="py-16 md:py-24 bg-[#0F0F0F]">
<div className="max-w-[800px] mx-auto px-4 xl:px-0">
<h1 className="text-[30px] md:text-[40px] font-normal tracking-[-2%] leading-[1.1] font-sans mb-4">
<span className="text-[#FFFFFF99] font-light">From the </span>
<span className="text-white">Blog</span>
</h1>
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans mb-16">
Thoughts on billing infrastructure, usage-based pricing, and building
for AI startups.
</p>
{posts.length === 0 && (
<p className="text-[#FFFFFF66] text-center py-16 font-light">
No posts yet. Check back soon.
</p>
)}
<div className="flex flex-col gap-1">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="group flex items-center gap-6 border border-[#292929] hover:border-[#3f3f3f] hover:bg-[#080808] transition-colors duration-300 p-6 md:p-8"
>
<div className="flex flex-col gap-3 flex-1 min-w-0">
<div className="flex items-center gap-3 font-mono text-[12px] md:text-[14px] uppercase tracking-[-2%] text-[#FFFFFF66]">
<span>{formatDate(post.date)}</span>
<span className="w-1 h-1 bg-[#FFFFFF44] rounded-full" />
<span>{post.author}</span>
</div>
<h2 className="font-sans text-[18px] md:text-[22px] tracking-[-2%] leading-[1.25] font-normal text-white group-hover:text-[#9564ff] transition-colors duration-300">
{post.title}
</h2>
{post.description && (
<p className="text-[14px] md:text-[16px] leading-5 text-[#FFFFFF99] font-light font-sans">
{post.description}
</p>
)}
</div>
{post.image && (
<div className="relative hidden sm:block w-[140px] md:w-[180px] aspect-[3/2] overflow-hidden shrink-0">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover"
/>
</div>
)}
</Link>
))}
</div>
</div>
</div>
);
}

View File

@@ -26,6 +26,7 @@ body {
} */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
/* ==========================================================================
RAW TOKENS — single source of truth
@@ -236,3 +237,98 @@ body {
.grid-overlay::after {
right: 20%;
}
/* ==========================================================================
BLOG PROSE OVERRIDES — dark theme, matches landing-page tokens
========================================================================== */
.prose.prose-invert {
--tw-prose-body: rgba(255, 255, 255, 0.6);
--tw-prose-headings: #ffffff;
--tw-prose-lead: rgba(255, 255, 255, 0.7);
--tw-prose-links: #9564ff;
--tw-prose-bold: #ffffff;
--tw-prose-counters: rgba(255, 255, 255, 0.6);
--tw-prose-bullets: rgba(255, 255, 255, 0.4);
--tw-prose-hr: #292929;
--tw-prose-quotes: rgba(255, 255, 255, 0.7);
--tw-prose-quote-borders: #9564ff;
--tw-prose-code: #e0e0e0;
--tw-prose-pre-code: rgba(255, 255, 255, 0.6);
--tw-prose-pre-bg: #141414;
--tw-prose-th-borders: #292929;
--tw-prose-td-borders: #292929;
font-family: var(--font-geist-sans);
font-weight: 300;
}
.prose.prose-invert p {
font-size: 14px;
line-height: 1.7;
letter-spacing: -0.02em;
}
@media (min-width: 768px) {
.prose.prose-invert p {
font-size: 16px;
}
}
.prose.prose-invert h2 {
font-weight: 400;
font-size: 22px;
letter-spacing: -0.02em;
line-height: 1.2;
margin-top: 2.5em;
}
@media (min-width: 768px) {
.prose.prose-invert h2 {
font-size: 28px;
}
}
.prose.prose-invert h3 {
font-weight: 400;
font-size: 18px;
letter-spacing: -0.02em;
line-height: 1.25;
margin-top: 2em;
}
@media (min-width: 768px) {
.prose.prose-invert h3 {
font-size: 22px;
}
}
.prose.prose-invert strong {
color: #ffffff;
font-weight: 400;
}
.prose.prose-invert li {
font-size: 14px;
line-height: 1.7;
letter-spacing: -0.02em;
}
@media (min-width: 768px) {
.prose.prose-invert li {
font-size: 16px;
}
}
.prose.prose-invert li::marker {
color: rgba(255, 255, 255, 0.4);
}
.prose.prose-invert a {
text-decoration-color: rgba(149, 100, 255, 0.4);
transition: color 0.3s, text-decoration-color 0.3s;
}
.prose.prose-invert a:hover {
color: #b08aff;
text-decoration-color: #b08aff;
}

View File

@@ -0,0 +1,89 @@
import Link from "next/link";
function BlogHeading({ as: Tag, children, ...props }) {
return (
<Tag {...props} className="scroll-mt-24">
{children}
</Tag>
);
}
export const mdxComponents = {
h1: (props) => <BlogHeading as="h1" {...props} />,
h2: (props) => <BlogHeading as="h2" {...props} />,
h3: (props) => <BlogHeading as="h3" {...props} />,
h4: (props) => <BlogHeading as="h4" {...props} />,
a: ({ href, children, ...props }) => {
const isExternal = href?.startsWith("http");
if (isExternal) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
{...props}
>
{children}
</a>
);
}
return (
<Link
href={href || "#"}
className="text-[#9564ff] hover:text-[#b08aff] underline underline-offset-2 transition-colors"
{...props}
>
{children}
</Link>
);
},
pre: ({ children, ...props }) => (
<pre
className="rounded-lg border border-[#292929] bg-[#141414] p-4 overflow-x-auto text-sm leading-relaxed"
{...props}
>
{children}
</pre>
),
code: ({ children, ...props }) => {
const isInline = typeof children === "string";
if (isInline && !props.className) {
return (
<code className="rounded bg-[#1c1c1c] border border-[#292929] px-1.5 py-0.5 text-[0.875em] text-[#e0e0e0] font-mono">
{children}
</code>
);
}
return <code {...props}>{children}</code>;
},
blockquote: ({ children, ...props }) => (
<blockquote
className="border-l-2 border-[#9564ff] pl-4 italic text-[#FFFFFF99]"
{...props}
>
{children}
</blockquote>
),
hr: (props) => <hr className="border-[#292929] my-8" {...props} />,
table: ({ children, ...props }) => (
<div className="overflow-x-auto my-6">
<table className="w-full border-collapse text-sm" {...props}>
{children}
</table>
</div>
),
th: ({ children, ...props }) => (
<th
className="border border-[#292929] bg-[#141414] px-4 py-2 text-left font-medium text-white"
{...props}
>
{children}
</th>
),
td: ({ children, ...props }) => (
<td className="border border-[#292929] px-4 py-2" {...props}>
{children}
</td>
),
};

View File

@@ -17,7 +17,7 @@ const footerColumns = [
links: [
{ label: "OUR TEAM", href: "#" },
{ label: "OUR VALUES", href: "/privacy" },
{ label: "BLOG", href: "https://useautumn.com/blog" },
{ label: "BLOG", href: "/blog" },
],
},
{

View File

@@ -26,7 +26,7 @@ import { DashboardIconPixel } from "./dashboard-icon-pixel";
const NAV_LINKS = [
{ label: "Docs", href: "https://docs.useautumn.com/welcome", Icon: IconDocs },
{ label: "Blog", href: "https://useautumn.com/blog", Icon: IconBlog },
{ label: "Blog", href: "/blog", Icon: IconBlog },
{ label: "Pricing", href: "#pricing", Icon: IconPricing },
{
label: "Discord",
@@ -375,11 +375,12 @@ export default function Navbar() {
<div className="flex flex-col">
{NAV_LINKS.map((item) => {
const isAnchor = item.href.startsWith("#");
const isExternal = item.href.startsWith("http");
return (
<Link
key={item.label}
href={item.href}
target={isAnchor ? undefined : "_blank"}
target={isExternal ? "_blank" : undefined}
onClick={isAnchor ? (e) => {
e.preventDefault();
setMenuOpen(false);

View File

@@ -110,6 +110,19 @@ export default function PricingModels() {
</div>
</div>
<div className="flex lg:hidden flex-col px-4 py-12 relative z-10">
<h2 className="text-[28px] sm:text-[32px] leading-[1.1] font-sans tracking-[-2%] font-normal">
<span className="text-[#FFFFFF99]">Any pricing model.</span>{" "}
<span className="text-white">Seriously.</span>
</h2>
<div className="mt-4 text-[15px] font-sans leading-relaxed tracking-[-1%] font-light">
<span className="text-[#FFFFFF99]">
Configure in the dashboard or CLI.
</span>{" "}
<span className="text-[#FFFFFF99]">Rollout to all customers, or create custom plans for your largest customers.</span>
</div>
</div>
<div className="border-t-0 lg:border-t border-[#292929] w-full relative grid grid-cols-1 lg:grid-cols-[60px_220px_220px_1fr] xl:grid-cols-[90px_291px_300px_1fr] auto-rows-auto lg:grid-rows-[200px_260px] xl:grid-rows-[260px_340px]">
<div className="hidden lg:block border-l border-r border-b border-[#292929] min-h-[120px] lg:min-h-[260px]"></div>

View File

@@ -36,16 +36,16 @@ export default function Problem() {
<div className="flex flex-col mb-2 gap-3 pl-[28px] xl:pl-[90px] pr-6 xl:pr-8 pt-14 sm:pt-16 xl:pt-16 items-center xl:items-start">
<h2 className="font-normal tracking-[-4%] leading-[32px] xl:leading-[40px] mb-2 xl:mb-4 text-center xl:text-left">
<span className="block text-[#686868] text-[30px] md:text-[36px] xl:text-[40px]">
Your coding agents
Hard to ship,
</span>
<span className="block text-white text-[30px] md:text-[36px] xl:text-[40px]">
can't build billing
harder to scale.
</span>
</h2>
<p className="text-[#888888] font-light text-[16px] md:text-[18px] xl:text-[16px] tracking-[-2%] leading-[20px] mb-8 xl:mb-10 max-w-sm md:max-w-lg xl:max-w-sm text-center xl:text-left">
Maintaining payment logic, customer balances and feature access
across pricing and product changes is months of work.
<span className="text-white"> Autumn replaces all of this.</span>
across pricing and product changes is months of work and unreliable.
<span className="text-white"> Autumn replaces all the billing code you're building yourself.</span>
</p>
</div>

View File

@@ -160,11 +160,12 @@ export default function ProductionScale() {
</h2>
</div>
<p className="text-[#FFFFFF99] font-light text-[16px] lg:text-sm lg:w-sm leading-[20px] lg:leading-5">
Autumn handles billions of billing events monthly. Open source core,
self-host ready,{" "}
Autumn is trusted by some of fastest-growing teams. Open source core,
self-host ready.{" "}
<span className="text-white">
designed for teams that <br className="hidden lg:block" />{" "}
can&apos;t afford billing downtime.
We'll help you go live quickly
<br className="hidden lg:block" />{" "}
and get back to what's important.
</span>
</p>
</div>

View File

@@ -0,0 +1,95 @@
---
title: "Architecture to run 100 stripe cases in 1 endpoint"
description: "Handling stripes upgrades, downgrades, schedules, one off and subscriptions all in 1 endpoint."
date: "2025-06-11"
author: "Ayush, Autumn Co-Founder"
slug: "attach"
image: "/images/blog/attach.png"
featured: true
---
Were building an engine to run software pricing models. Something we still struggle to conceptualize is the number of cases that need to be handled. Take some common examples and how youd do it in Stripe:
**Scenario**
**Endpoint**
Upgrading a subscription
`POST /subscriptions/:subscription_id`
Scheduling a downgrade
`POST /subscription_schedules`
Creating a checkout session
`POST /checkout/sessions`
Creating a new subscription (if card is on file)
`POST /subscriptions`
Creating a one off payment
`POST /invoices/:invoice_id/pay`
There are more complex scenarios that require updating individual items within a subscription (eg. decrease the price of a metered feature when upgrading).
Stripes low-level design maps each action to a different function. When designing Autumn, we were pretty strong in our belief that all these cases should just be 1 endpoint: `POST /attach`
Initially we just handled basic cases, so a set of if else statements was enough. As weve started handling more cases, the if else spaghetti was becoming a nightmare of bugs. We spent last week rewriting the architecture into 5 steps so we can handle it more logically.
### Step 1: Input validation and parsing
This is the body that the `/attach` request takes in, and handles all request related errors. We use Zod to parse the overall schema, then use its `refine` method for more granular error throwing (eg if conflicting fields are passed in).
### Step 2: Building the AttachContext
Using the inputs, we then make all the DB queries and calculations we need to gather the necessary data. These include:
1. The product data → its prices and features it gives access to
2. The customer data → Their current product, existing configuration, payment method details
3. Data from the request body → eg. checkout session params
### Step 3: AttachBranch
This step is a first order categorisation of the `/attach` function based on the request body and context.
In our previous architecture, interpretability was a mess. We had our branching logic in multiple files and no concrete control / understanding of which path attach runs given the inputs.
We realised that different branches could be run through the same function. For instance, add ons and new subscription products branches, while separate scenarios, can now both be routed to the same `addProduct` function (see step 5). However they can also be routed to `createCheckout` depending on the config params in the next step.
![](https://framerusercontent.com/images/YcEatrdJWheMwGEWAyQB7Bw6hqY.png)
### Step 4: AttachConfig
These are a set of parameters that control the specific behavior of the product enablement. They have default values determined by the previous stages of the pipeline.
For instance, we can control:
- whether an upgrade is prorated, or charged in full
- whether a new product should create a checkout session, charge a default payment method or just generate an invoice
- whether any existing meter usage should carry over to the new product, or be reset
![](https://framerusercontent.com/images/z4LFqNDl2GGJLBbvNjvfbec7UQ.png)
### Step 5: AttachFunction
The last step of the attach call is to determine which function to run, based on all of the prior categorisations. We referred to this in the AttachBranch step.
For instance, updating a custom product and upgrading to a new product technically can go through the same function, just with different configs (eg. proration behavior), and therefore can be routed to the same function — “UpdateProduct”
![](https://framerusercontent.com/images/XJzahBCPpL01rgJQOvOD5k1hoc.png)
Then within each attach function, we make all the relevant calls to Stripe to ensure the scenario occurs successfully.
And then of course for each of these cases, you need to handle the downstream logic of actually giving the customer what theyve paid for, which usually involves listening to webhooks, updating some permissions and maybe reseting usage limits.
We made that into another endpoint (`check`)… but thats a story for another day.

View File

@@ -0,0 +1,76 @@
---
title: "We tried to make billing backendless"
description: "We wanted to make payments, billing, usage limits and tracking as easy as possible. We explored a billing integration that required no backend setup, but ultimately gave up."
date: "2025-05-13"
author: "Ayush, Autumn Co-Founder"
slug: "backendless"
image: "/images/blog/backendless.jpeg"
featured: true
---
[Read the updated version of this article](/blog/thanks-hn)
We think handling payments on the frontend is a better developer experience.
Typically, billing is a backend job and requires webhooks, state syncing, then passing the data to the frontend. We wanted to offer a more "out-of-the-box" experience when handling things like payment links, paywalls and up/downgrade flows, and spent a bunch of time thinking about how we can perform sensitive operations without needing to perform the "round trip" to the backend.
This is a short write up of our exploration around the problem and why we ultimately are giving up.
**Part 1: The Publishable Key**
When we launched, we had a secret key that could be used securely from the backend just as [Stripe](/blog/what-if-stripe-builds-this) does. Many of our first users had actually never set up Stripe before, and immediately told us they wish they could just do it from the frontend.
Our first solution was to create a "publishable key" which would let developers get payment links and check feature access (eg, does my user have any remaining credits) directly from the frontend, in an unprotected way. These functions alone can't really be abused.
The initial response was good and people were happy to use it with their initial set up. But we quickly ran into a couple problems:
1. It only worked with some endpoints (eg, tracking billable usage events had to be done via the secret key) and ended up confusing devs around which endpoints could be used with which keys.
2. Most software billing flows allow you to automatically purchase something if you've made a purchase before. This automatic purchasing (eg for upgrades) definitely couldn't be done with a public key.
3. Although it helped people spin up a sample integration fast, it quickly had to be ripped out anyway, so ended up being pretty pointless.
This still exists in our docs today and constantly trips people up. Can't wait to get rid of it.
**Part 2: Server Actions**
When we launched our Next.js library, we were excited to use server actions. The DX felt magical because users could:
1. Call them from the frontend like any normal function
2. The functions run on the server and can access our secret key stored as an ENV variable
3. No route set up needed, and the request is secure — nice!
Unfortunately we soon discovered our approach was flawed. Server actions are public, unauthenticated routes, and our API calls updates resources based on a `customer_id` field (eg. upgrade / downgrade requests, tracking usage for a feature, etc).
So if you got a hold of someone elses customer ID, you could make requests to the public server actions as if you were that customer—making this method insecure.
**Part 3: Server actions + encryption**
We really really liked the DX of server actions though, and so we had to brainstorm a way to overcome the customer ID being expoed in server action routes.
A few options came to mind, like using a middleware, or registering an authentication function, but the cleanest and simplest method we thought of was simply encrypting the customer ID:
Heres how it worked:
1. Our Provider was a server component, and so itd take in a customer ID/user ID (server side), encrypt it using their API key, and pass it to context on the client side (see image below)
2. We wrap each server action with a client side function which grabs the `encryptedCustomerId` from context and passes it to the server action. These are all exported through a hook — `useAutumn`
3. Each server action first decrypts the customer ID then calls the Autumn API
![](https://framerusercontent.com/images/lr6FGInFULdwXgxESSi825vE.png)
Essentially, we baked our own layer of auth into the server actions, and this is how our Next.js library works today. I haven't seen this approach by anyone else but essentially lets us fully handle secure payments, upgrades, downgrades, usage-based billing events — all from the frontend.
Were still not fully satisfied since this only works with frameworks that support server actions and SPA / vite is kinda making a comeback. It also makes the implementation different across frameworks.
**The future**
Ultimately I think we'll reach a point where we give up on this approach, and move towards a more framework agnostic approach. Rather than trying to abandon the backend route setup, we'll just make it easy to do. Take better-auth and how they generate their backend routes in just a couple lines of code — theyve standardised the backend and frontend installation, and is pretty hard to get wrong.
Other companies like Polar have a similar approach and are heavily praised for it. We probably don't need to reinvent the wheel (as fun as it is).

View File

@@ -0,0 +1,151 @@
---
title: "React vs shadcn, and trying to be too clever"
description: "Learnings about building a component library for a billing platform for AI companies"
date: "2025-06-30"
author: "Ayush, Autumn Co-Founder"
slug: "component-fail"
image: "/images/blog/component-fail.png"
featured: true
---
You know those moments where you have an idea so brilliant you feel like Steve Jobs? Launching our component library felt like that. Novel, high risk, and a radically different approach to what was on the market. If it worked out, it'd be incredible.
Unfortunately, it didn't. We ended up with a weird mess and had to spend the last week rewriting everything.
_For context: we're building Autumn, pricing and billing software. Part of our offering is frontend components for things like a pricing table, paywalls etc. You can read more about them_ [_here_](https://docs.useautumn.com/quickstart/shadcn)_._
### **V1 as a React library**
We first attempted this as a standard npm React library:
```
import PricingPage from "autumn-js"
```
It's biggest issue was customisability**.** There are two ways to make npm components customisable: through props, or a no-code interface in our app.
1. With props there were too many class variables. For example with the pricing page, each pricing card is composed of many headers, descriptions, buttons and more. And each card itself can have variants (recommended, discounts, annual etc).
2. A low code interface is a bad experience for devs, who want full customisability and have 10x experience designing with tailwind/css. Even more so with the era of cursor. This is subjective but Ive never had a good experience with one.
_Sidenote:_ If you ever try to make your npm component customisable via tailwind, all the best. That was one of the most frustrating days of our lives.
Also, If you ever forgot what jsx styles look like, heres a snippet of what we had to write…
![](https://framerusercontent.com/images/AfIf7siCuQZQhQWWGgBmQQjoyjw.png)
We made one measly post on linkedin about it, decided it was unusable and shelved it.
### **V2 as shadcn/ui components**
A few weeks later we started looking into it again with shadcn.
```
npx shadcn@latest add https://ui.useautumn.com/classic/pricing-table.json
```
This immediately felt like a more customizable and fluid DX, but had its own challenges:
1. Because shadcn components install as a user file, we cannot fully control it via our SDK. For example, if we wanted to trigger a modal/popup to confirm a plan upgrade, the user needs to explicitly pass it into a function. This does take away slightly from that “magical DX”.
```jsx
//upgrading to Pro tier
<Button
onClick={async () =>
await attach({
productId: "pro",
dialog: ProductChangeDialog,
})
}
/>;
```
2. Since users own and control the component files, deciding what data abstraction level to return to the frontend is hard. For example, our upgrade dialog shows different text and styling depending on the scenarios:
\- One time purchase vs subscription
\- Upgrades vs downgrades vs cancellations vs renewals
\- Does the upgrade require an input from users (eg, quantity of credits to purchase)
![](https://framerusercontent.com/images/ncBPxNIGQkh4vZHzdi87FAweR0.png)
With a React library, everything would be "completely processed", with no customizability. Initially this is the same approach we took with the shadcn registry, but users quickly told us they wanted to customize the text too. We switched to the "in-between" approach.
Our API will return a scenario (eg, upgrade, downgrade, add-on, free-trial etc), and our shadcn components install with a library of cases that users can edit to control the messaging.
```jsx
switch (scenario) {
case "scheduled":
return {
title: <p>Scheduled product already exists</p>,
message: <p>You will downgrade on {scheduled_date}</p>,
};
case "active":
return {
title: <p>Product already active</p>,
message: <p>You are already subscribed to this product.</p>,
};
case "new":
if (recurring) {
return {
title: <p>Subscribe to {product_name}</p>,
message: (
<p>
By clicking confirm, you will be subscribed to {product_name} and
your card will be charged immediately.
</p>
),
};
}
//..... etc
```
![](https://framerusercontent.com/images/qQ6pvBXw6yquIFtUptD2lRggE.png)
However, the shadcn library turned out as a huge mess for several reasons:
**Mistake 1**
We decided to launch our components as a shadcn package instead of plain React. Our components approach was inspired by Clerk, but I always thought they looked out of place and wanted users to own them. Shadcn had launched registry functionality so people could share and download components, and [Supabase](https://supabase.com/ui) launched one too, so it seemed promising.
Unfortunately only half our users use shadcn, but the others wanted components and couldn't use them. More importantly, we were iterating quickly on the underlying API that controls the component content. Since the components weren't synced to our SDK, every update we made broke integrations. We should have kept it simple with React to start, then expanded to shadcn once stable.
![](https://framerusercontent.com/images/8zVRLo7wQpddNJ8o4uRaDmom2BY.png)
**Mistake 2**
We launched a separate open source pricing component library called [pricecn](pricecn.com). We thought it would go viral like [React Email](https://react.email/) by Resend. We designed it so pricecn users could easily migrate to using Autumn components later later, and so made the Autumn component library have the pricecn library as a dependency.
Pricecn didn't take off. People liked generating pricing cards from JSON, but it's hard enough to promote one project, let alone two. Having Autumn components depend on pricecn ones was just a confusing experience for users, as each component came with several files in different folders.
![](https://framerusercontent.com/images/B1eGaHr7YRlAxGzl1TNCz0n38A.png)
**Mistake 3**
We launched with 3 different styles: classic, clean and dev. I thought this would grow adoption since people could pick what suits them.
Again, people liked the concept but it made maintenance hell. Every issue meant jumping into 3 component files, so we kept putting off fixes until it became unusable.
![](https://framerusercontent.com/images/rWKl6Maa9Y596cIMNwJhUaAL6og.png)
**What we have now**
1. We launched a React library as the core experience, but allowing users to download the same files as shadcn components to customize it
2. We only kept the "classic" style and simplified it.
3. We cut the dependency on pricecn.
We kept the shadcn components since people liked owning them when they worked, and now our API is a little more stable, it's working better.
Maybe we're still trying too hard to be clever but here's the cool part: instead of maintaining two sets, our shadcn library automatically replicates from our React ones so they always stay synced. Here's the script that syncs them whenever we `npm run dev`:
[https://github.com/useautumn/autumn-js/blob/main/package/scripts/sync-registry.ts](https://github.com/useautumn/autumn-js/blob/main/package/scripts/sync-registry.ts)
![](https://framerusercontent.com/images/RsmW3zsJedW8xAhbTJljALEvylI.png)
To be honest we have no idea how that script works but it does. Thanks Claude Code!

View File

@@ -0,0 +1,56 @@
---
title: "How we made our first sale as a startup"
description: "How we generated our first revenue as a startup, and got into Y Combinator."
date: "2025-01-23"
author: "Ayush, Autumn Co-Founder"
slug: "how-we-made-our-first-sale-as-a-startup"
image: "/images/blog/how-we-made-our-first-sale-as-a-startup.jpeg"
featured: true
---
We worked on onboarding automation for fintechs throughout the YC F24 batch and made reasonable progress. We made our first sale the week before we flew to SF.
It was pretty simple. We saw there were some old companies in the space building the rule engines that fintechs use to determine whether to approve or deny a sign-up (for fraud/compliance reasons). We thought we could do this better with AI (though not clear how) and that itd be faster to try sell this to early-stage companies.
We went on LinkedIn and reached out to about 100 operations leaders at early-stage fintechs (15-50 headcount). We got a bite from a small payment provider based in the UK. Heres the message we used.
![](https://framerusercontent.com/images/PdKH3L6qwIPP0Ll5FMJidwnD5BM.webp)
**First call**
We hopped on a call with them that week. They wanted to launch self-serve onboarding and had a super manual process of onboarding customers over email. They said they wanted to get this sorted that quarter. The timing was perfect.
**Second call**
After a bit of follow-up, they looped in the CEO, who asked us a few more questions about our background. We demoed a front-end version of our drag-and-drop signup form builder + rule engine, said it worked, and that we knew what we were doing.
_**“Lets do it”**_
I couldnt believe that was enough to land a 2000 USD / month contract within a week. We agreed to help them analyze their onboarding data, come up with a good sign-up flow, decide on approve/deny rules to implement, and build everything.
They wanted a consultancy/dev shop—it was definitely more of a services contract. We were content either way and thought we were “off to the races”. 100 outbound messages = a 2k contract? This was going to be easy…
We sent them a Stripe invoice link that day and the invoice was paid the following day. I remember it feeling almost underwhelming at the time. I didnt feel as happy as I thought I would as my goalpost immediately shifted to closing the next sale.
But after weeks of that same process, days of long and mind-numbing outreach, we couldnt replicate it. We managed to sign some contracts towards the end of the batch (although Im pretty convinced they did it as a favor to us with demo day coming up), lost conviction, and decided to pivot. Thats a story for another time with a bunch of learnings.
### First sale with Autumn
With Autumn our approach was less sales-heavy from the start. We just wanted to talk to founders and see what their problems were. This time we had the YC community at our disposal, so emailed about 150 founders of product-led companies to chat about the tools they used. We knew we wanted to try building in that space, optimizing for a company wed enjoy building as this is what we struggled with when building Recase.
![](https://framerusercontent.com/images/o0SAJx7y6ZV4zWVMqFqkwkX8zo.webp)
One fairly high-profile company responded and mentioned that billing was a nightmare for them. Their users paid for credits, and they wanted a system that could outsource all the credit balance and payment logic.
After speaking to a few more founders we decided to mockup a prototype in a Figma alternative (Subframe), went back to this same founder, and presented it. He was super chilled and agreed to pay us 400 USD a month for it.
If I had to pay forward the learnings from my very nascent sales career it would be:
- You probably dont need to write any code to sell something. But you almost definitely need to show something that demonstrates you can solve the problem. That can be a mockup, but think about who youre demoing to. Are they a founder who is okay with seeing something early? Are they a compliance person who need to get the impression of a fully formed product to take a chance?
- Getting the first sale can be pretty easy if you talk to people and stumble on an acute problem someone has at the right time.
- The question is whether multiple people fit that same persona, have the same problem, and want the same thing. Its tempting to get that sale and run 100% towards it which is the approach we took, but a safer one is probably to make multiple sales before building. People who tried to build in this space and failed advised us to do exactly this. If you can do this youve likely uncovered the makings of a massive company. The trade-off is youll likely spend a lot longer in the idea maze (aka pivot hell), which can be shit.
Its been 8 months for us and were still figuring it out. You decide what works best for the company you want to build.

View File

@@ -0,0 +1,100 @@
---
title: "Marketing is hard"
description: "A little write up on our thoughts and exploration around how to market our developer tool for billing and payments."
date: "2025-06-17"
author: "Ayush, Autumn Co-Founder"
slug: "marketing"
image: "/images/blog/marketing.png"
featured: true
---
Are founders ever happy with their product? There are **so many** different bugs, ideas, features and requests that I would love nothing more than to spend all my hours on. Designing product is my happiest state.
Unfortunately, while I like to believe I have good taste, the product is the easy part. Getting people aware of what we're doing and convincing them its worth trying is what matters in today's world.
90% of my life goes into thinking about GTM. This is a write up of what we're exploring and some useful resources.
### Who are we targeting?
When we initially launched Autumn, we were targeting seed and series A VC backed startups. Our GTM was cold outbound, main KPI was revenue. It was actually growing pretty well. But we realized 2 things:
1. Getting people to integrate was heavily timing dependant
2. There was a small segment of inbound users using it for side projects
We pivoted to make a bigger bet on a longer term strategy of becoming the next go-to in payments. Since April, our strategy has been to target day 0 builders and founders.
### How are we doing it?
Our retained users are growing ~11% week on week. It's a broad, bottoms-up GTM.
![](https://framerusercontent.com/images/wzYESJZefwP8SvFkpzzgZoCtaw.png)
The main channels we spend time on are:
**X/Twitter**
This is where most people find us. We've had some success posting demos and launches on @johnyeo\_'s account (2K followers) and have a smallish following (600 followers) on @autumnpricing.
Our accounts are active (2-5 posts per day) and have reasonable engagement. 3 months ago, we had ~200 followers in total. If we were starting again we would:
- Follow relevant accounts in our ICP, and **other small active accounts trying to grow**. YC is an advantage here as we already have a network of people like this.
- Engage with these accounts a lot: 100+ replies per day, and follow even more accounts.
- People will follow you back and engage with you too, which is good for the algo. I personally follow back anyone who (1) follows me and (2) engages with me.
- Possibly even start a group chat to help keep accountable and comment, repost and bookmark each other consistently (if you're doing this, we'll join!)
_Note: all of this only really works because there are people that like our product and are interested in what we're building. If you don't have a likeable product you should fix that first before shouting about it_
Our most successful post was honestly total nonsense clickbait: a 1 minute demo where we built "cursor for stripe". When you spend enough time on twitter, you start to get a sense for what a good tweet looks like and what the "current thing" is. AI video content doing something novel works the best, with a snappy tweet that jumps straight into it.
Going viral is great. When it works, it works well, but is super variable. Chasing virality over and over again is a repeatable GTM for consumer apps, but harder in the tech twitter sphere. A lot of the time it feels like shouting into the void.
[Resend's heartbeat framework](https://resend.com/handbook/marketing/how-we-keep-momentum) resonates a lot with us. We have to constantly be building, writing about it, and shipping.
**Hacker news**
We had 1 blog post of ours that hit front page of HN for about 3 hours, and it drove more traffic to our site than we'd seen in the whole month. Even though most of it was negative (people found the title to be misleading), it drove a lot of sign ups.
**Blogging (like this)**
Our blogs probably get about 300-400 reads per week, which we're pretty happy with as we've just started. We're trying to lean more into creating content that we'd find interesting, hoping that other founders and devs also like it. When we write, we share it on twitter, our email newsletter, and a few relevant subreddits.
**Word of mouth**
We're fortunate enough that some people have really experienced the pain we solve and like what we're doing. Because of that, we often find ourselves being recommended in twitter threads, and people saying friends said good things about them. This is one that feels more out of our hands.
Other than a few upcoming platform launches (Launch HN, Product Hunt), we want to try a few more channels properly like LinkedIn and Reddit. The advice we've heard is that you never really know what's going to work for your startup, so try a lot of things.
We are also thinking about how we can bake marketing into our product a bit more (eg Autumn branded receipts on purchase).
### What is our message?
I used to think this wasn't important, then I heard Ant (Supabase founder) talk on the scaling devtools podcast about how they went from 8 hosted DBs to 800 over a weekend, just by changing their messaging from
Real-time postgres DB -> Open source firebase alternative
And launching again on HN.
<iframe width="100%" height="400" src="https://www.youtube.com/embed/ptC_FTZ1hc0" frameBorder="0" allowFullScreen></iframe>
This is one we're working on. Some angles we've gone for are:
- Any pricing model in 6 lines of code
- Never deal with billing again
- Open source infrastructure for pricing and billing
- What supabase did for Postgres, we're doing to Stripe (?)
A lot of devtools took off by iterating on the product over time, and really crystallizing who they're targeting. Shoutout to Clerk who grinded for 3 years, then found success by going all-in on frontend, Next.js devs. Polar.sh (another payments company) had a lot of success by pairing their setup with better-auth.
### Anyway
Marketing is a slog. It's trial and error, but with a long and hard to measure feedback loop. I wish we could be one of those teams that just builds it and they come. But until we hit PMF we'll just keep shitposting 🫡

View File

@@ -0,0 +1,50 @@
---
title: "Post-mortem: Database outage caused by collation migration locking conflict"
description: "Post-mortem write up of an outage that affected a number of requests between 15:00 and 16:00 UTC, 15th March 2026."
date: "2026-03-15"
author: "Ayush, Autumn Co-Founder"
slug: "post-mortem-database-outage-caused-by-collation-migration-locking-conflict"
image: "/images/blog/post-mortem-database-outage-caused-by-collation-migration-locking-conflict.png"
---
On Sunday March 15, 2026 from 15:00 to 16:15 GMT, approximately 10% of API requests to Autumn failed due to a database outage caused by a collation migration on our production database.
## Timeline
- **~15:00** — Collation changes applied to `customer_entitlements` and `customer_products`. DB CPU spikes, queries begin failing.
- **15:05** — Issue identified. Attempted rollback via `ALTER TABLE` but blocked by active queries holding locks, causing repeated deadlocks.
- **15:20** — Attempted code-level fix: updated the main customer query to explicitly cast collation, hoping to force index usage without a schema rollback.
- **15:48** — With help from PlanetScale support, shut down all application connections, reverted collation changes, and ran `ANALYZE` to rebuild planner statistics.
- **16:15** — Full recovery confirmed across both regions. All APIs and dashboard operational.
## What happened
We were optimizing our most frequently run queries and discovered that several indexes weren't being used due to a collation mismatch between parent and child tables (customers.internal\_id uses COLLATE "C" for KSUID ordering, but FK columns like customer\_entitlements.internal\_customer\_id used the default collation). Postgres can't use indexes across this mismatch, so our most frequent query was taking ~150ms when it should have been &lt;10ms.
After changing the collation on customer\_entitlements, we saw CPU drop by 30% and much faster query times. To prevent this issue in the future, we decided to align collations across all related tables. There were 4 tables to change, in this order:
1. customer\_entitlements
2. customer\_products
3. entities
4. customers
After changing collation on the first two tables, our main query's performance dropped dramatically due to missing indexes, and this made our DB CPU spike and queries fail. We tried to reverse the changes, but the ALTER TABLE needs an exclusive lock on the table, and there were always active queries holding locks — causing repeated deadlocks.
## Resolution
We first tried to push a code-level fix (aligning collations in the query SQL itself), but this didn't take effect fast enough with the DB under heavy load. Ultimately, we shut down all application connections, reversed the collation changes, ran ANALYZE to rebuild planner statistics, and restarted. Queries returned to normal immediately and CPU leveled off.
## Prevention & Remediation
All future column/schema migrations will be done using the duplicate-dual-write-cutover pattern. Instead of altering a column in place (which requires an exclusive lock and a full table rewrite), we create a new column with the desired type, dual-write to both columns, backfill existing rows in small batches, create indexes concurrently, then cut over.
More generally, we will be adding a status page to improve transparency when we have issues. We are also considering making a change to our `check` and `track` SDKs that would enable them to fail-open by default, so that in an outage there is no disruption to paying customers.

View File

@@ -0,0 +1,111 @@
---
title: "Embedded UI components are easier with shadcn"
description: "We decided to use a shadcn/ui registry to set up our pricing components, instead of a Reactjs library. We used this approach for pricing pages, upgrade and downgrade flows and paywalls."
date: "2025-05-28"
author: "John, Autumn Co-Founder"
slug: "shadcn"
image: "/images/blog/shadcn.png"
featured: true
---
We're building Autumn, which is a billing platform that helps devs integrate [stripe](/blog/what-if-stripe-builds-this) and manage their pricing model. One feature of this is a UI library to drop-in things like a pricing table, upgrade/downgrade flows, paywalls--similar to Clerk with auth.
We are heavy users of shadcn/ui and love having full design customisability. When Supabase launched its UI library through shadcn, we wanted to do the same.
Here's our exploration into it and some of the challenges we faced.
### **V1 as a React library**
We first attempted this as a standard npm React library:
```
import PricingPage from "autumn-js"
```
It's biggest issue was customisability**.** There are two ways to make npm components customisable: through props, or a no-code interface in our app.
1. With props there were too many class variables. For example with the pricing page, each pricing card is composed of many headers, descriptions, buttons and more. And each card itself can have variants (recommended, discounts, annual etc).
2. A low code interface is a bad experience for devs, who want full customisability and have 10x experience designing with tailwind/css. Even more so with the era of cursor. This is subjective but Ive never had a good experience with one.
_Sidenote:_ If you ever try to make your npm component customisable via tailwind, all the best. That was one of the most frustrating days of our lives.
Also, If you ever forgot what jsx styles look like, heres a snippet of what we had to write…
![](https://framerusercontent.com/images/AfIf7siCuQZQhQWWGgBmQQjoyjw.png)
We made one measly post on linkedin about it, decided it was unusable and shelved it.
### **V2 as shadcn/ui components**
A few weeks later we started looking into it again with shadcn.
```
npx shadcn@latest add https://ui.useautumn.com/classic/pricing-table.json
```
This immediately felt like a more customizable and fluid DX, but had its own challenges:
1. Because shadcn components install as a user file, we cannot fully control it via our SDK. For example, if we wanted to trigger a modal/popup to confirm a plan upgrade, the user needs to explicitly pass it into a function. This does take away slightly from that “magical DX”.
```jsx
//upgrading to Pro tier
<Button
onClick={async () =>
await attach({
productId: "pro",
dialog: ProductChangeDialog,
})
}
/>;
```
2. Since users own and control the component files, deciding what data abstraction level to return to the frontend is hard. For example, our upgrade dialog shows different text and styling depending on the scenarios:
\- One time purchase vs subscription
\- Upgrades vs downgrades vs cancellations vs renewals
\- Does the upgrade require an input from users (eg, quantity of credits to purchase)
![](https://framerusercontent.com/images/ncBPxNIGQkh4vZHzdi87FAweR0.png)
With a React library, everything would be "completely processed", with no customizability. Initially this is the same approach we took with the shadcn registry, but users quickly told us they wanted to customize the text too. We switched to the "in-between" approach.
Our API will return a scenario (eg, upgrade, downgrade, add-on, free-trial etc), and our shadcn components install with a library of cases that users can edit to control the messaging.
```jsx
switch (scenario) {
case "scheduled":
return {
title: <p>Scheduled product already exists</p>,
message: <p>You will downgrade on {scheduled_date}</p>,
};
case "active":
return {
title: <p>Product already active</p>,
message: <p>You are already subscribed to this product.</p>,
};
case "new":
if (recurring) {
return {
title: <p>Subscribe to {product_name}</p>,
message: (
<p>
By clicking confirm, you will be subscribed to {product_name} and
your card will be charged immediately.
</p>
),
};
}
//..... etc
```
![](https://framerusercontent.com/images/qQ6pvBXw6yquIFtUptD2lRggE.png)
Ultimately, after having used it ourselves and watching our users, were pretty happy with this approach. Shadcn is as popular as it is for a reason, and we believe in the paradigm of owning your own components and everything inside them.
We're maintaining these components for free separate to our main product here: [https://pricecn.com](https://pricecn.com/)

View File

@@ -0,0 +1,138 @@
---
title: "Talking about billing with 30 YC founders"
date: "2025-01-22"
author: "Ayush, Autumn Co-Founder"
slug: "talking-about-billing-with-30-yc-founders"
image: "/images/blog/talking-about-billing-with-30-yc-founders.jpeg"
featured: true
---
We were in YCs F24 batch and pivoted the day before demo day (story coming soon). Since then weve been in the idea maze and spoke to 30 very kind YC (and some non YC) founders/leaders about their billing systems.
Why billing? This space is big and crowded for sure. But I used to work for a payments company and I find this stuff to be super interesting.
Were writing our learnings from this as an exercise both to start drawing interest in what were doing but also as a source of truth as we continue learning.
##### How do people manage billing?
As you can expect this depends on the stage of company and their billing model. Will distill it to these
**Seed Stage Companies**
_Sales-led:_
- Use Stripe for invoicing with manual setup
- Sometimes use simple, cheap tools for quick proposal generation in high-volume scenarios
_Product-led:_
- Rely on Stripe billing and payment links
- Implement feature flags and entitlements directly in code
**Series A Companies**
_Sales-led:_
- Have automated invoicing flows, integrated with CRM
- Use data tools for revenue data organization
_Product-led:_
- Some adopt billing tools like Orb or Metronome. Although usually noting that they used it because Stripes offering was bad _at the time_
- Maintain simple, code-based entitlement systems
**Series B-C Companies**
_Sales-led:_
- Finance teams take ownership of contract and pricing setup
_Product-led:_
- Maintain custom admin panels for feature management in bespoke contracts
- Update pricing once or twice yearly to experiment with optimization
- Focus on metrics such as paywall conversion rates
**Key learnings:**
- Stripe is everywhere (as expected) and getting better. It wasnt loved but it was fine.
- Entitlement management is useful for later stage sales led companies, but also for earlier-stage companies managing both product-led + sales led models.
- When a company was using a vendor, the push came either from finance wanting better data for revenue reporting, or CTOs wanting to save dev time on billing infra. No one (apart from one ex-intercom founder) really cared about experimenting with pricing as a lever to drive more revenue. Standard practice was just looking at competition and charging a market rate.
##### So what were the pain points?
As good as Stripe is, there were pain points that did come up repeatedly:
- Managing credit-based billing systems. There is no good tool out there for this today.
- Chasing up invoices that havent been paid. AI seems like a no-brainer here, but definitely a bunch of solutions already.
- Revenue recognition from Stripe. Usually around Series A there needs to be a big clean up for investors. People dont enter the right contract terms in Stripe when theyre first starting out which leads to messy data.
- Maintaining a billing system and making pricing changes. Especially if supporting multiple versions (grandfathering) and moving people between plans.
- Dealing with custom contracts for sales-led and product-led teams. Stripe doesnt seem to have great tooling for this, creating free trials etc.
- People had tried third-party billing software but dropped them when they couldnt support a specific nuance of their pricing model (eg dealing with upgrade / downgrade pro rata meters differently)
We didnt get the sense that many people would switch to a whole new billing system to solve any one of these problems. Something that is plugged into their existing system may work better?
##### **Lots of people have tried this**
As soon as we started looking into this space it was immediately clear that there were a lot of people in this space—and a lot of people who had tried and failed.
Honestly it doesnt faze us much anymore. This seems to be the state of SaaS markets in general and is the sign of a big market. From the people we spoke to who failed:
- Stripe billing has got pretty good recently and means it was hard to find customers who were interested in switching. Finding a customer is also super timing dependant and so will rely on a strong brand and inbound motion.
- Easier to get a few small customers who are starting out but moving up-market is hard, especially for a seed-stage company.
A random learning is that people seem to be pretty salty about their old ideas that have failed. Didnt seem very self-reflective on what they could have done better or differently. Just said the idea was stupid.
##### **What do we think?**
Were still forming our opinion on the space so this is a braindump of our current thoughts on the space. As much as we hope, its unlikely thisll be a rocketship from day 1.
But we are seeing more companies opt for off-the-shelf software like Clerk and Supabase. Greater preference for staying lean and moving faster.
Most companies are not going to switch from Stripe or another billing provider for this. These software are very engrained into the codebase and are generally good enough. And so while usage based pricing is increasing its still Stripe capturing the market share. We really need to think about our GTM. Who its for and how we get in:
- Having a modular suite of interconnected sales &lt;&gt; billing &lt;&gt; finance tools could be the way to wedge-in and expand within a larger company. Entitlement management seems like a great part of the stack to control.
- Or we could grow a brand for startups and make it super easy to implement billing.
- Or we could be the best ever billing provider for some type of company (eg credit companies)
There is also some growing dissent with Stripe as they get bigger and bigger. Implementation effort is pretty tough and summarised well by [t3.gg](http://t3.gg/) here. In our view our approach makes this way easier.
##### **More generally**
Different people working on the same idea with different initial conditions can have massively different results. Eg Pylon were the 13th team to have the same insight they did, but theyre killing it. I think theres some alpha in making a product people really love with a great brand around it.
Were excited to have signed a fast growing, high profile AI startup already and will try to ride their growth wave with them. Maybe thats the winning strategy over trying to time larger companies into switching billing systems and the one were leaning towards. But maybe well kick ourselves down the line for not focusing up-market. What we build will likely look different (more horizontal earlier, more vertical for up-market).
We were previously working on fintech onboarding software selling to compliance teams. Contract sizes were bigger there but also was super slow. For whatever reason right now were having a lot more fun exploring this and talking to people about it. It feels like a problem we can become obsessed with, do our best work, and execute well.
TLDR: we're going to keep going and are excited.
Shoutout to Granola for making it a lot easier to keep track of our notes and learnings from customer discovery
Will check in next month to see whats changed about what we think!

View File

@@ -0,0 +1,64 @@
---
title: "A roasting from HN makes better devtools"
date: "2025-05-20"
author: "Ayush, Co-Founder at Autumn"
slug: "thanks-hn"
image: "/images/blog/thanks-hn.png"
featured: true
---
At Autumn, we're trying to build an integrated billing platform that can run any software pricing model.
A lot of the integration lift is on the frontend, since we handle user payment flows (payment links, upgrades/downgrades, paywalls etc). Making the developer experience simple for new devs, while keeping product flexible has been our main focus over the past few months.
Now Stripe is typically a backend-heavy integration, with data being passed to the frontend. One of the first pieces of feedback we got from users was:
> I wish I could direct a customer to a payment URL without needing to generate it on the backend, and pass it to the frontend
We became fixated with trying to let users handle payments with as little backend involvement as possible. Here's what we tried and how a roasting from HN helped us arrived in a better place.
### Part 1: Frontend-only integration with a public key
Our first solution was to create a "publishable key" that could be used straight from the frontend, to get payment URLs and check feature permissions (eg user\_123 has 10 credits).
People were initially satisfied, but there was no way to safely handle things like downgrades and cancellations. So even though setup was faster, it had to be ripped out quickly.
### Part 2: Nextjs server actions
Many of our users apps were built with Next.js, which has a feature called "server-actions". We were excited as it enabled our users to make calls on the frontend, and have them run from the backend. We thought this would solve our problem.
We didn't know much about how Next.js worked, and quickly found out that similar to our public key, server actions are just public endpoints to their own backend server. Since all our requests are based on a `user_id`, it meant that anyone with access to it would be able to perform billing actions.
### Part 3: Reinventing JWTs (badly)
In a moment of genius, we realized we could just encrypt the `user_id`. Users would pass it to us via a Next.js server-side component, we'd encrypt it with their API key, and use it from the client side to authenticate requests.
We posted this approach on Hacker News last week, and got promptly roasted for (amongst other things), simply reinventing JWTs, without token refresh and with more steps.
> Isn't it enough that this startup seems destined to failure? No need to beat a dead horse
Ouch. Thankfully the best part about a startup is you get to learn quickly.
### Part 4: Throwing it all out
The incentive for this exploration had been to spare the user from setting up backend routes. But we'd ended up with:
- A pretty poor developer experience, especially outside of Next.js
- An insecure integration, as if you knew the encrypted customer id, you'd again have full billing permissions
Over the last few days we rewrote the whole library. Instead of reinventing the wheel, we made it easy to set up Autumn cleanly and safely:
- All functions are called from the backend.
- We wrote middleware for each framework to spin up these routes (to make a purchase, check feature permissions, open the billing portal, track billing usage etc).
- We hook into the existing auth system through this middleware, by providing an identify function where the user can decrypt their JWTs into the user\_id
![](https://framerusercontent.com/images/yqrINW5SeAFvnhxywyOEHHBD710.png)
We're happier with this for now, although set up is slightly longer and possibly less "magical". It's a lot clearer what happens on the client, what happens on the server, and where to use each package.
Thanks HN for the tough love.

View File

@@ -0,0 +1,84 @@
---
title: "People really don't like metered billing"
description: "Replits 2026 pricing shift highlights three trends in AI-first products: usage-based billing creates anxiety, seats dont map to AI value, and credits/effort-based pricing reduce transparency as agents run longer."
date: "2026-01-26"
author: "Ayush, Autumn Co-Founder"
slug: "usage-billing-bad"
image: "/images/blog/usage-billing-bad.png"
featured: true
---
Replit announced a big pricing change recently, and it put words to a few patterns Ive been noticing across AI-first products. TL;DR:
- As agents run longer and more autonomously, pricing gets harder.
- People dont like usage-based billing because they cant predict it.
- Per-seat pricing doesnt map cleanly to AI value.
From this mess, the pricing model that has prevailed is the "credit": a pre-purchased token for "work".
## Users hate usage-based billing
Usage-based billing is “fair” in theory. You pay for what you use. In practice, especially for UI-first products, it creates background anxiety.
Every click feels like it might cost money, so you explore less, and subconsciously pause before taking each action. You feel out of control of your end of month bill.
This is why you keep seeing products move away from metered billing and more towards **prepaid credits** (a monthly bucket, and usually one-off packs on top). It doesnt necessarily change the underlying economics, but it changes the product experience: controlled spend with a hard cut-off.
Even in APIs / infra, where usage-based has been the standard, we're seeing auto-topups replacing pure usage-based (OpenAI being a good example).
The direction of travel is the same: when AI costs are high and variable, predictability and hard-limits are necessary.
## Per-seat pricing is basically dead for AI-first products
Replits new Pro plan reflects this shift. Instead of $40/seat/mo, they've introduced a team-wide pool of credits, with a seat/collaborator cap (up to 15 builders).
Even for something like Cursor, where the price on their site is displayed "per-seat": what you're _actually_ paying for is the $40 USD of AI usage per-month that come with the seat -- not the seat itself.
In traditional SaaS, seats are a decent proxy for value. More people using the tool generally means more value. In AI-first products, the marginal value often isnt “another seat.” Its:
"how much work can the model/agent do for us?"
Replit has also brought “real collaboration” into Core (up to 5 people), and theyre sunsetting the old Teams plan. Importantly, this its a product strategy move:
- Bake collaboration into the default experience.
- Make “usage” the thing you sell, rather than individual power users.
- Put a cap on seats, but make the variable—and the real value—how much work gets done.
## Pricing is getting less transparent as agents run longer
The more autonomous agents become, the harder it is to keep pricing legible. Replit is a clean example.
> Simple tasks may cost less than $0.25, more complex tasks may cost more than $0.25
They used to charge a fixed $0.25 per checkpoint. Simple mental model that you could predict it. Now its "effort-based": you get charged _some random amount_ based on time + compute for the request.
Mechanically, this makes sense. Agent requests vary wildly: a tiny change versus a long-running debugging session are not the same unit of work. But UX-wise, it shifts the feeling.
And credits add a second layer of opacity. You see a credit balance, but the mapping is fuzzy. How do model tokens map to Replit credits? Whats the range for a “normal” request? What's replit's markup on the credits?
When your costs are unpredictable you need some way of passing it on to your customers. Credits are great because they can effectively grant users the same number of credits, or even double them -- but silently increase their margins however they want.
## What's next?
Credits will probably stick around for a while because theyre the best abstraction we have right now. But they also feel like a patch over the harder question:
How do you make AI spend feel predictable when agents can do wildly unpredictable amounts of work?
I would personally love to see more AI products prioritize **billing observability**:
- flat costs for simple tasks
- estimates _before_ you run expensive tasks (“this will be ~X credits based on history”)
- a receipt _after_ you run (“this cost X because **\_”)**
- ranges + forecasting (“at this pace youll run out on **\_”)**
That would make AI feel more like software and less like a casino. But my guess is that uncertainty will just become part of the new software contract.

View File

@@ -0,0 +1,40 @@
---
title: "We're making some API changes"
description: "A bit of context about what will be changing in the new version of the Autumn API and SDK."
date: "2026-02-18"
author: "Ayush, Autumn Co-Founder"
slug: "we-re-making-some-api-changes"
image: "/images/blog/we-re-making-some-api-changes.png"
featured: true
---
Over the last year we learned a lot about how you use Autumn. We have noticed which pain points come up repeatedly and where users face limitations. Our v2 SDK will soon be released in beta, which we think offers a more intuitive and flexible experience as you build on Autumn.
**There will be no breaking API changes:** everything is versioned on our end and your current version will continue to be supported. 
### Whats changing?
**New objects / types.** 
Our customer and plan objects are changing to better suit our abstraction:
- Any reference to `product` is now `plan` to better reflect our abstraction
- Customer products are now split into `subscriptions` and `purchases` for easier access
- Features that a customer has access to will now be under a `balances` object
- Subscription status can now only be one of `active`, `scheduled` or `expired` - with additional params to determine trialing, canceled and past due states
- Types are more consistent (eg, consolidation of `one_off` and `lifetime` intervals)
**Attach is being split**
Today, the `attach` function is used for any operation that changes subscription state — checkouts, upgrades, downgrades, quantity updates, trial updates etc. This has become unpredictable to work with for you guys, and almost impossible for us to maintain. Adding functionality in one flow almost always breaks another.
Were splitting this into 2 functions: `attach` will be used for operations that result in a plan change (checkout, upgrades etc), and `update` will be used for operations that update an existing customer subscription (cancelations, quantity adjustments, trial adjustments).
We know API changes are annoying, but we're biting the bullet now to set ourselves up for better stability in the long run. Again, your existing integrations will not break and we will continue to support the current version. For those of you that want to upgrade, well be providing a short prompt/skill for Claude that can migrate your code for you.
As part of this, weve refactored a lot of code, which has allowed us to release a lot of new functionality and configurabilty. More details on that soon. Thank you all for helping make Autumn better ❤️‍🔥

View File

@@ -0,0 +1,65 @@
---
title: "What if Stripe builds this"
description: "Stripe has been the go-to for billing, subscriptions and managing payment logic for 16 years. We think that'll change soon."
date: "2025-06-02"
author: "Ayush, Autumn Co-Founder"
slug: "what-if-stripe-builds-this"
image: "/images/blog/what-if-stripe-builds-this.png"
featured: true
---
The "what if `${incumbent}` builds this" question is usually asked as a meme. Really the only time it comes up is when founders make fun of crappy VCs. For some reason though, we hear this a lot. I think it's because:
1. People don't quite understand what we do. Which is fair enough. It's quite new and we're still not great at explaining it.
2. Stripe is still totally revered in the Startup ecosystem.
They've been around since 2009 and have had a complete monopoly over the early stage software market for over 16 years. There is no comparable company that can say the same:
- AWS (2006) -> Supabase
- Heroku (2007) -> Vercel or Render
- Twilio (2008) -> Resend
- Okta (2009) -> Clerk
- DataDog (2010) -> Sentry
The tech stack has tended to higher levels of abstraction over time. And while there has been competition targeting later-stage companies (eg for usage-billing, CPQ solutions), the early stage software market is still dominated by Stripe.
However, they've grown to a size where cracks are starting to show. They're less focused on the early stage segment (take a look at r/stripe if you're not convinced). And new devs struggle to set it up.
Because Autumn abstracts away all that complexity, people are starting to like what we do. If it continues to work, I'm sure Stripe will make a play here. However I still think we have a pretty good shot.
### 1\. We'll always have a better product
From a high level it seems like we're a couple features on top of Stripe, and I will concede a fair amount of our current value is represented by that. But truly solving this problem is complex and will take years to get right.
Creating a platform that is simple to start with, but is flexible enough to run any pricing model is an infinite matrix of logic, scenarios, payment states, edge cases etc. This only gets harder and harder as we move upmarket.
Technically speaking it's a totally different challenge too: at scale we look more like a database (eg Supabase/Convex), or feature flagging system like LaunchDarkly. This is pretty far away from what Stripe is good at.
That's not to say that the best product always wins. But in devtools it certainly matters more than in other product categories.
### 2\. Developers (and companies) like not being locked in
And there's 2 ways we play into that:
1. We're open source
2. We're not tied to Stripe. We can build our own system, and build integrations with Adyen, Chargebee, Checkout.com etc. This gives enterprises bargaining power with their payment providers.
So even if they decide to throw out Stripe and rebuild exactly what we're doing, we still have a pretty strong place in the market.
### 3\. The market is very big
The market size for billing tools is around $10 billion and growing every year. Selling into a growing market is great because there are always new and interesting problems to focus on.
We're making a particular bet on early stage product-led companies, but every single company we've spoken to had their own unique problems. Whether in this space, earlier stage in the funnel during the sales process (CPQ), or later stage in the funnel (finance operations).
The timing feels right and I think what we're building is a good bet. That said Stripe if you wanna acquire us we'll still hear your offer 😙

View File

@@ -0,0 +1,101 @@
---
title: "Consumer psychology is important in AI pricing (feat. T3 chat)"
description: "We worked with Theo and Mark from T3 Chat to build a cool new way of pricing. We learned about the psychology behind how AI products are consumed, and what that means for how we should monetize them."
date: "2026-02-20"
author: "Ayush, Autumn Co-Founder"
slug: "working-with-t3-chat-on-a-new-way-of-pricing"
image: "/images/blog/working-with-t3-chat-on-a-new-way-of-pricing.png"
featured: true
---
[T3 Chat](https://t3.chat/) is the best alternative to AI apps like ChatGPT and Claude, created by [YouTuber Theo](https://www.youtube.com/@t3dotgg) and co-founder Mark. It gives you access to all the latest AI models in one place, is a beautifully designed product and has become my daily-driver for AI. It was no surprise to me that they have several 10s of thousands of subscribers.
Initially, the subscription gave access to 1500 "standard" messages, and 100 "premium" messages per month, where premium messages were reserved for more expensive models. We helped them move to a multi-tier, waterfall rate limits model.
It was a pretty interesting insight into the psychology behind how AI products are consumed and what that means for monetization.
### FORO
The first reason behind the change is to address what Theo calls "fear of running out". Whenever users hit enter to send a message to T3, there's a latent anxiety that comes with watching your usage counter slowly tick up.
Almost no users ever hit their limit of 1500 messages, but that didn't really matter -- they were paranoid that they would.
Similar to a video game where you hoard your items til the final boss, users would subconsciously attempt to ration their usage for the month, in case they needed it. The feeling of being limited was often cited as a reason that a user didn't convert.
With the new pricing model, users now have a bucket of usage credits that replenishes every 4 hours. You either use it or lose it, making you feel less guilty about consuming it. If your quota runs out, you simply wait a few hours before coming back.
![](https://framerusercontent.com/images/kTES2qA50b4nRULRUX1JQLUspA.png)
### Prepaid credits > metered pricing
Economically, prepaid credits and metered billing can be identical. A user might consume $6 worth of AI in a month either way. But the experience of spending that $6 is completely different.
With metered pricing, every message feels like a purchase decision. Users are constantly running a background cost-benefit analysis: "is this prompt worth it?"
Prepaid credits flip this. Once the money is spent, using the product stops feeling like spending and starts feeling like redeeming. It becomes "I should use what I've already paid for."
Replit is a great example of a company that just made this change, introducing prepaid credit tiers in their latest pricing change.
Metered is still great for predictable, infrastructure-level usage (cloud compute, API calls at scale) where buyers are sophisticated and want to pay for exactly what they use. But for products where you want users to engage freely and habitually, prepaid credits seem to have become the preferred billing method.
### Variable usage patterns
Users will often send messages in short, frequent sessions. In these cases, each session only uses a small number of tokens.
Other times, usage can be more "bursty", when working on a bigger task. If the 4 hour quota was the only available option, this type of usage would be often interrupted and users would need to wait.
To get around this, some of the monthly quota is assigned to a monthly overage bucket. If a certain session uses more than the 4 hour limit, it will start drawing from this bucket instead.
The challenge with moving to this model was previously, users were able to by lifetime top-up messages, and we needed a way to still honor this purchase. As part of the migration, we converted these prepaid top-up messages into a standalone credit balance that lasts forever. This bucket of messages will be drawn from last in the waterfall.
![](https://framerusercontent.com/images/e7xtV5kyN9erOK99cg6joWWmVTY.png)
[OpenAI's billing system](https://openai.com/index/beyond-rate-limits/) for Codex and Sora works in a very similar way. The team also added a "premier" tier for $50, with 10x more usage, better suited for power users that were previously purchasing multiple top ups.
### Unpredictable costs
Messages are not created equally. Each would consume a fixed quota, but the underlying costs would vary dramatically depending on the input context. Some user behaviors were painfully expensive:
- Dumping many files from their codebase into the chat
- Using expensive models to analyze PDFs and images
- Continuing long threads with expensive models, instead of starting new ones
Any of these actions could cost several dollars each and a small (but regular) proportion of the user base ended up being seriously unprofitable.
The team switched to a credits system. Every model and action (eg web search) has an underlying credit cost associated with it.
When a message is requested, an estimated number of credits for that message is prematurely deducted from the user's balance (ie, reserving balance). After the message is completed, an adjustment event is sent to either refund (if the estimate was higher) or capture additional credits (if the estimate lower). Reserving credits before the message is sent protects against overconsumption.
A secondary benefit of this system is that users can now use premium and basic models as they wish interchangeably.
![](https://framerusercontent.com/images/cWI7p7DZOf1joAQDExcxGHsDM0.png)
### How Autumn helped
Before using Autumn, the team had followed [Theo's viral guide](https://github.com/t3dotgg/stripe-recommendations) on how to manage Stripe. Subscription statuses and balances of messages were stored in a Redis instance that ran on upstash. Even though it's the easiest setup to manage, they were still dealing with race conditions around failed payments and race conditions.
With the increase in complexity of the new pricing model, they decided that they didn't want to be slowed down by billing anymore. Using Autumn meant they didn't have to deal with stripe code, webhooks, waterfall credit deduction, cron jobs, cache layers, failed payments / 3DS, etc. All while having full flexibility to change their rate limits at any point, without code changes or migrations.
> Stripe is a hassle. It's an amazing platform with a lot of capability, but with that capability comes a ton of little details that you have to manage.
>
> Autumn ticked all of our boxes and allowed us to consolidate a bunch of services into one place, and with a team we trust to know the intracacies and do it right.
>
> We were spending nearly as much on the infrastructure to handle this ourselves (less well) than the cost with autumn. It makes the switch even more of a no brainer.
\-Mark (T3 Chat Co-Founder)
### Thinking about doing something similar?
The specific limits and buckets will still be refined, but having used it myself over a couple weeks, it really does feel great to use and is more profitable for the company. To build a similar model:
1. Define the average margin per user you're looking to make, and how many credits a user can use each month to hit that. Eg, to make $2 on a $8 sub, you have $6 of credits an average user can use.
2. Allocate ~30-50% of those credits to the monthly overage bucket
3. With the remaining, divide them across the average number of sessions a user has in a month
This will give you a good place to start. Your users will very quickly tell you how they feel about it, and you can refine it from there.

View File

@@ -0,0 +1,62 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const CONTENT_DIR = path.join(process.cwd(), "content", "blog");
export function getAllPosts() {
if (!fs.existsSync(CONTENT_DIR)) return [];
const files = fs
.readdirSync(CONTENT_DIR)
.filter((file) => file.endsWith(".mdx"));
const posts = files.map((filename) => {
const filePath = path.join(CONTENT_DIR, filename);
const raw = fs.readFileSync(filePath, "utf-8");
const { data } = matter(raw);
return {
slug: data.slug || filename.replace(/\.mdx$/, ""),
title: data.title || "Untitled",
description: data.description || "",
date: data.date || null,
author: data.author || "Autumn Team",
image: data.image || null,
};
});
return posts.sort((a, b) => {
if (!a.date || !b.date) return 0;
return new Date(b.date) - new Date(a.date);
});
}
export function getPostBySlug({ slug }) {
if (!fs.existsSync(CONTENT_DIR)) return null;
const files = fs
.readdirSync(CONTENT_DIR)
.filter((file) => file.endsWith(".mdx"));
for (const filename of files) {
const filePath = path.join(CONTENT_DIR, filename);
const raw = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(raw);
const fileSlug = data.slug || filename.replace(/\.mdx$/, "");
if (fileSlug === slug) {
return {
slug: fileSlug,
title: data.title || "Untitled",
description: data.description || "",
date: data.date || null,
author: data.author || "Autumn Team",
image: data.image || null,
source: content,
};
}
}
return null;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB