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
| Part | Field | Example | Decide it by |
|---|---|---|---|
| Price | price | $29 | What the buyer pays today to solve the problem |
| Interval | interval | month | once, day, week, month or year |
| Entitlements | Offer.grant | Collection.export | The Verbs the buyer may call |
| At the bound | atBound | upgrade | stop, overage or upgrade |
| Window | availableFrom, availableUntil | Launch week only | Unset 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:
---
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:
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:
| Offer | Price | Features | Limits | At its bound | On sale |
|---|---|---|---|---|---|
Starter (starter) | $15 per seat, billed monthly | Dashboard.ask | Collection.export: 10 per rolling day | upgrade to Pro | Always |
Pro (pro) | $49 per seat, billed monthly | Dashboard.ask, Collection.export, SSOConnection.activate | None. | None. | Always |
An Entitlement bounded by a Limit has kind limit; one without has kind feature. The kind is computed, never set.
atBound | A call past the Limit |
|---|---|
upgrade | Refused 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.
atBound | The call past the Limit | Use it when |
|---|---|---|
stop | Refused with a 402 that names the Limit | The Limit is the promise, such as one deliverable or two revisions |
overage | Allowed, and each unit past the Limit bills through the Meter | Usage is the value, as with API calls or lookups |
upgrade | Refused with a 402 that names the next Offer up | A 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.
| Property | Stripe field | Source of truth |
|---|---|---|
price | default_price | db.sb |
A new Price is a new Offer. Raise my price shows how existing customers keep theirs.