# Lock a feature to a plan

URL: /products/offers/lock-a-feature-to-a-plan · Id: `guide:lock-a-feature-to-a-plan`

> Say "lock export to the paid plan" and only customers on that plan can export; the rest meet an upgrade prompt, or a 402 that names the plan.

An [Entitlement](/business/nouns/entitlement) on an [Offer](/business/nouns/offer) says which [Verb](/business/nouns/verb) its customers may call. Every screen, API call and agent call checks it, so the lock holds everywhere at once.

Needs: [Charge monthly](/systems/finance/charge-monthly) · [Open a bank account](/systems/finance/open-a-bank-account)

**Before it runs**

- You leave the chat: Never: it all happens in the chat.

## What you get
- 1 Entitlement on the Pro Offer for export (Entitlement)
- 1 upgrade prompt where a customer without it tries to export (Page)
- 1 refusal for an API or agent call without it, naming the plan that covers it (Offer)
- 1 row on the pricing page comparison (Offer)

## Your agent does
**For agents**

- Guide: `guide:lock-a-feature-to-a-plan`, status `live`. Page: [/products/offers/lock-a-feature-to-a-plan.md](/products/offers/lock-a-feature-to-a-plan.md).
- Approval: none. Run it when the founder asks.
- Leaves the chat: never.
- Needs first: [Charge monthly](/systems/finance/charge-monthly), [Open a bank account](/systems/finance/open-a-bank-account).
- Run: call the steps in order, each through the API, SDK, CLI or MCP tab below; a later step passes the `$id` an earlier one returns. Each value marked example is the sample business’s: pass the founder’s own.

| Step | Verb | Inputs (type, required, this guide’s value) | Returns |
| --- | --- | --- | --- |
| 1 | [`Offer.grant`](/business/nouns/offer/grant) (`verb:Offer.grant`) | `offer` Offer (an id), required: example `"offer_pro_monthly"`; `verb` Verb (an id), required: example `"Collection.export"` | `entitlement` Entitlement, `offer` Offer |

Returns: the last call answers with `entitlement`, `offer` and its `action`, whose `status` is `done`, `pending` (waiting on an Approval or a Worker), `failed`, `canceled` or `refused`:

```json
{
  "api": "https://api.sb/leaseline.app",
  "entitlement": {
    "$id": "https://api.sb/leaseline.app/entitlements/entitlement_1",
    "$type": "Entitlement",
    "verb": "Collection.export"
  },
  "offer": {
    "$id": "https://api.sb/leaseline.app/offers/offer_pro_monthly",
    "$type": "Offer"
  },
  "action": {
    "$id": "https://api.sb/leaseline.app/actions/action_1",
    "operation": "Offer.grant",
    "status": "done"
  },
  "links": {
    "self": "https://api.sb/leaseline.app/actions/action_1"
  }
}
```

Next: offer “Give Starter export too, ten a day”, or “Let trial customers export once”.

Calls: `Offer.grant`

Ask:

```text
Lock export to Pro.
Only Pro customers can export.
Make CSV export a paid feature.
```

API:

```bash
# Offer.grant
curl -X POST https://api.sb/leaseline.app/offers/offer_pro_monthly/grant \
  -H "Authorization: Bearer $STARTUP_BUILDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"verb":"Collection.export"}'
```

SDK:

```ts
import { $ } from 'startup-builder'

await $.Offer.grant({ offer: 'offer_pro_monthly', verb: 'Collection.export' })
```

CLI:

```bash
npx startup-builder do Offer.grant --json '{"offer":"offer_pro_monthly","verb":"Collection.export"}'
```

MCP:

```json
{
  "name": "do",
  "arguments": {
    "code": "return await $.Offer.grant({ offer: 'offer_pro_monthly', verb: 'Collection.export' })"
  }
}
```

## Change it
- Give Starter export too, ten a day.
- Let trial customers export once.
- Show the upgrade button where export is locked.

## Under the hood
- Systems: [Billing](/systems/billing), [Matchmaking](/systems/matching)
- Nouns: [Entitlement](/business/nouns/entitlement), [Offer](/business/nouns/offer), [Customer](/business/nouns/customer)
- Events: [`Offer.granted`](/business/events/Offer.granted), [`Entitlement.checked`](/business/events/Entitlement.checked)