Skip to content

Anatomy of an Offer

An Offer is five decisions: a Price, how often it bills, the Entitlements it grants, what happens at a Limit, and when it is on sale.

An Offer is five decisions: a Price, how often it bills, the Entitlements it grants, what happens at a Limit, and when it sells. Make them once, and the pricing page, checkout, Portal and API all follow.

The five parts

PartFieldExampleDecide it by
Priceprice$29What the buyer pays today to solve the problem
Intervalintervalmonthonce, day, week, month or year
EntitlementsOffer.grantCollection.exportThe Verbs the buyer may call
At the boundatBoundupgradestop, overage or upgrade
WindowavailableFrom, availableUntilLaunch week onlyUnset means always on sale

Most first Offers need three of the five: a Price, an interval and one Entitlement. Add a Limit when a customer's usage costs you money, and a window only for a launch or a seasonal deal.

Written once

An Offer is one file in the Startup's repository, and its frontmatter is the Offer.create input. Leaseline's paid plan:

offers/pro.mdx
---
price: { amount: 29, currency: USD }
interval: month
atBound: upgrade
---

Its Entitlements are granted one Verb at a time. Pro grants the dashboard, and export up to 100 times a day:

TypeScript
import { $ } from 'startup-builder'

const daily = await $.Limit.create({ count: 100, period: 'day', resets: 'rolling' })
await $.Offer.grant({ offer: 'offer_pro', verb: 'Dashboard.ask' })
await $.Offer.grant({ offer: 'offer_pro', verb: 'Collection.export', limit: daily.$id })

Every Product Template starts a Startup with default Offers in this shape. Here are the two a SaaS starts with:

OfferPriceFeaturesLimitsAt its boundOn sale
Starter (starter)$15 per seat, billed monthlyDashboard.askCollection.export: 10 per rolling dayupgrade to ProAlways
Pro (pro)$49 per seat, billed monthlyDashboard.ask, Collection.export, SSOConnection.activateNone.None.Always

An Entitlement bounded by a Limit has kind limit; one without has kind feature. The kind is computed, never set.

atBoundA call past the Limit
upgradeRefused with 402 payment-required, naming the next Offer, which Entitlement.check returns as upgrade.

Offer.publish puts each Offer on every buy surface: checkout, paymentLink, pricingTable, portalEntry, mcpBuyTool, http402.

Features and limits

An Entitlement with no Limit is a feature: the customer may call the Verb as often as they like. An Entitlement with a Limit is a limit: a count or an amount over a period.

A Limit resets one of two ways. A calendar Limit starts fresh at the top of each period, so 50,000 calls a month resets on the first. A rolling Limit counts the last period from now, so 10 a day means 10 in any 24 hours.

Use rolling for abuse protection, and calendar for anything a customer budgets for.

Checking access covers how every call is checked, and the one call your own code makes.

At the bound

atBound says what a call past a Limit gets. Every Offer with a Limit declares it, and no founder approval is needed to set it.

atBoundThe call past the LimitUse it when
stopRefused with a 402 that names the LimitThe Limit is the promise, such as one deliverable or two revisions
overageAllowed, and each unit past the Limit bills through the MeterUsage is the value, as with API calls or lookups
upgradeRefused with a 402 that names the next Offer upA self-serve plan with a bigger plan above it

Default to upgrade on self-serve plans and overage on usage plans. Surprise bills lose customers faster than a polite upgrade prompt.

Where it sells

Offer.publish puts one Offer on every buy surface at once. That is checkout on your Domain, a payment link, the pricing table and the Portal. It is also a buy tool on your MCP server, and the 402 reply an agent can accept.

Stripe holds the Offer as its own Product, and the Price as Stripe's Price:

Product (product). The source of truth is db.sb, and Stripe holds a copy written from it.

PropertyStripe fieldSource of truth
pricedefault_pricedb.sb

A new Price is a new Offer. Raise my price shows how existing customers keep theirs.