Imported from Salable/salable-claude-code-plugin (
plugins/salable/skills/salable-monetize/SKILL.md). Install upstream withnpx skills add Salable/salable-claude-code-plugin --skill salable-monetize. Copyright stays with the author.
Salable Pricing Model Builder
Design and implement Salable 2.0 monetization. Use MCP tools for catalog provisioning writes; use REST API for app runtime surfaces (pricing pages, entitlement checks, subscription management).
Codebase Context Rules
- Work exclusively with files on disk. Do NOT use git log, git diff, or commit history to understand the project.
- Read actual source files for stack, structure, auth, and existing integrations.
Salable MCP Prerequisite
Before any provisioning work, check whether Salable MCP tools are available (e.g. mcp__salable__products_list).
If available: You MUST use MCP for all catalog writes. Do not fall back to REST for provisioning.
If not available: Stop and instruct the user:
- Export API key:
export SALABLE_API_KEY="your_key"(add to~/.zshrcfor persistence). - The plugin already configures the MCP server. If it's not connecting, check that
SALABLE_API_KEYis set in your environment. - Restart Claude Code and re-run
/salable:salable.
Do not proceed without a working MCP connection.
OpenAPI Spec
The openapi.yaml is truncated in browsers/WebFetch but the full spec is available via curl. At session start, download it:
curl -s https://salable.app/openapi.yaml > /tmp/salable-openapi.yaml
Always grep this file for exact request/response schemas before writing integration code — do not guess field names or casing. Use salable.app contracts only, not docs.salable.app.
Default Monetize Scope
When asked to "monetize/monetise this app", deliver three things:
- Public (unauthenticated) in-app paywall/pricing table.
- In-app feature gating with entitlements.
- In-app subscription management views (authenticated).
Determine entitlement definitions and plan-to-feature mapping with the user before writing catalog resources.
Security Gate
Run before any catalog writes or app integration:
- Verify app has real authentication. Identify the session principal for billing identity mapping.
- Never use raw emails in identity fields (
owner,grantee,granteeId). Prefer: org/tenant id > stable internal user id > non-emailuser.id. Last resort: deriveusr_${hmac_sha256(email, salt)}. - Emails are fine for contact fields (e.g. checkout
email), not identity ids. ownermust be the app's external principal, never Salable internal ids likeowner_....- If auth is missing: stop, recommend auth for the detected stack (e.g. Auth.js for Next.js, Auth0/Clerk for React SPA, Better Auth for Node.js), and do not proceed until auth is in place.
- Public pricing views may remain unauthenticated; checkout, entitlement checks, and subscription management require authenticated identities.
User Discovery Checklist
Before any provisioning, gather all of the following from the user. Do not assume or skip items.
- Features & entitlements — What gated features? What entitlement keys? (e.g.
export_pdf,team_collab) - Plans — What plans? (e.g. Free, Starter, Pro) Display names?
- Plan-to-entitlement mapping — Which entitlements does each plan grant? Confirm the full matrix.
- Line item types per plan — Flat-rate? Per-seat? Metered? (Max one
per_seatper plan.) - Pricing amounts — Exact amounts per line item. Currency (default USD).
- Billing intervals — Monthly? Yearly? Discounts for longer intervals?
- Multi-currency — Non-USD currencies needed? Amounts?
- Seat/usage limits — Min/max seats? Metered tier breakpoints?
Present a summary table for confirmation before building:
Plan | Entitlements | Line Items | Monthly | Yearly
Starter | export, basic_api | flat_rate $29 | $29/mo | $290/yr
Pro | export, full_api, sso | flat_rate + seat | $49 + $12/seat | $490 + $10/seat
Workflow
- Download OpenAPI spec —
curl -s https://salable.app/openapi.yaml > /tmp/salable-openapi.yaml - Auth preflight — Confirm auth exists and identify non-email identity source. Exit if missing.
- User discovery — Walk through checklist. Present summary table for confirmation.
- Build in dependency order via MCP: entitlements -> product ->
plans_saveper plan -> verify withplans_get. - Verify — Re-read persisted prices to confirm amounts match intent. Return resource IDs.
- Verify schemas before app code — Grep
/tmp/salable-openapi.yamlfor exact request/response schemas of every endpoint you will call. - Build app integration — After catalog provisioning, immediately build the app-side surfaces. Do NOT stop after provisioning and wait for the user to ask. See the App Integration section below.
App Integration
After catalog provisioning, build all of the following automatically. Read existing files first to avoid duplicating code that already exists; create or update as needed.
Required deliverables
-
Salable API utility — Server-side helper (
salableApi) withcache: 'no-store'and Bearer auth. Identity bridge function (getSalableIdentity) mapping the app's session principal to a non-email Salable identity. -
Entitlements provider — Client-side React Context that fetches entitlements on session change and exposes
has(key),loading, andrefresh(). Must be mounted in the root layout inside the auth session provider. -
API routes — All route handlers calling Salable MUST include
export const dynamic = 'force-dynamic':GET /api/entitlements— checks grantee entitlements via Salable REST, returns{ entitlements }.POST /api/checkout— creates cart -> adds cart-item -> initiates checkout -> returns{ url }.GET /api/subscription— lists active subscriptions for the authenticated owner.POST /api/subscription/portal— creates a Stripe billing portal session, returns{ url }.
-
Pricing page (public, unauthenticated) — Displays all plans with feature comparison. Free plan links to sign-up or builder. Paid plans trigger checkout (redirect to sign-in first if unauthenticated). Already-subscribed users see "Manage Subscription" linking to account page. Expose the plan ID via
NEXT_PUBLIC_SALABLE_PRO_PLAN_ID(or similar env var per plan). -
Account / subscription management page (authenticated) — Shows current plan status (entitlement-based), included features list, billing portal button (payment method, invoices, cancellation). Handles
?checkout=successredirect from Stripe with a success banner and entitlement refresh. WrapuseSearchParams()in a<Suspense>boundary for Next.js static generation compatibility. -
Navigation wiring — Add Pricing link to the main nav. Add Account link to the authenticated user header area (next to sign out).
-
Auth protection — Ensure the account page route is protected by auth middleware. Public pricing page remains unauthenticated.
-
Environment variables — Update
.envand.env.examplewith all required Salable env vars including provisioned plan IDs.
Build verification
After building, run next build to verify compilation. Fix any errors (Suspense boundaries, missing imports, type errors) before presenting the result to the user.
MCP Tools Reference
| Action | Tool |
|---|---|
| Create/list/update product | products_create, products_list, products_update |
| Create/list entitlements | entitlements_create, entitlements_list |
| Save complete plan (preferred) | plans_save |
| Read/list plans | plans_get (with expand), plans_list |
| Inspect line items/prices | line_items_list, prices_list, currency_options_list |
| Archive resources | products_archive, plans_archive, line_items_archive, prices_archive |
All prefixed with mcp__salable__. Use plans_save as the default write path.
REST Runtime Endpoints
- Pricing page (public):
GET /api/plans,GET /api/products - Entitlement check:
GET /api/entitlements/check - Subscriptions:
GET /api/subscriptions,GET /api/subscriptions/{id},GET /api/subscriptions/{id}/invoices - Billing portal:
POST /api/subscriptions/{id}/portal - Plan changes:
PUT /api/subscriptions/{id}/items - Lifecycle:
POST /api/subscriptions/{id}/cancel,PUT /api/subscriptions/{id}/auto-renew
Response Wrapping
All REST responses are wrapped in a data envelope. Always unwrap through .data:
- Object endpoints (entitlement check, portal, single GET):
{ "type": "object", "data": { ...payload } }— accessbody.data. - List endpoints (subscriptions, plans):
{ "type": "list", "data": [ ...items ], "hasMore": bool }— accessbody.data(the array).
Common mistake: body.entitlements instead of body.data.entitlements, or body.url instead of body.data.url.
Checkout Flow
Three sequential API calls. All required fields must be present or the call returns 400.
Step 1: Create Cart — POST /api/carts
| Field | Required | Notes |
|---|---|---|
owner |
Yes | Your app's billing identity. NOT an email. |
interval |
Yes | day, week, month, year. Must match plan price. |
intervalCount |
Yes | e.g. 1 for monthly. Must match plan price. |
currency |
No | ISO currency code. Defaults to plan's default. |
Step 2: Add Cart Item — POST /api/cart-items
| Field | Required | Notes |
|---|---|---|
cartId |
Yes | ID from step 1. |
planId |
Yes | The Salable plan ID. |
interval |
Yes | Must match cart. |
intervalCount |
Yes | Must match cart. |
grantee |
No | User receiving entitlements. For solo: same as owner. Goes here, NOT on the cart. |
metadata |
No | JSON object for seat quantities etc. |
Step 3: Checkout — POST /api/carts/{id}/checkout
| Field | Required | Notes |
|---|---|---|
successUrl |
Yes* | *Required unless set in product settings. |
cancelUrl |
Yes* | *Required unless set in product settings. |
email |
No | Pre-fill Stripe checkout email. |
Returns: { "data": { "url": "https://checkout.stripe.com/..." } } — redirect user to body.data.url.
Entitlement Check — GET /api/entitlements/check
Query params: granteeId (required), owner (optional).
Access entitlements: body.data.entitlements — each item has value (the key), type, and expiryDate.
The entitlement key is
value, NOTname. Always map features withe.value.
Billing Portal — POST /api/subscriptions/{id}/portal
Creates a Stripe billing portal session. Returns body.data.url — redirect the user there.
| Field | Required | Notes |
|---|---|---|
features |
Yes | Object with camelCase keys. Must include at least one sub-feature. |
returnUrl |
No | Redirect URL after portal session. |
features sub-features (camelCase only — snake_case causes 500):
| Key | Required fields |
|---|---|
paymentMethodUpdate |
enabled: boolean |
subscriptionCancel |
enabled: boolean, when: "now" | "end" |
invoiceHistory |
enabled: boolean |
customerUpdate |
enabled: boolean, allowedUpdates: ("name"|"email"|"address"|"phone"|"shipping")[] |
Each sub-feature has its own required fields — omitting when from subscriptionCancel or allowedUpdates from customerUpdate causes 500 errors.
{
"features": {
"paymentMethodUpdate": { "enabled": true },
"subscriptionCancel": { "enabled": true, "when": "end" },
"invoiceHistory": { "enabled": true }
},
"returnUrl": "https://app.example.com/account"
}
Grantee & Group Model
Entitlement access chain: Grantee -> Membership -> Group -> Subscription Plans -> Entitlements.
- If
granteeis passed on the cart-item, Salable auto-creates the grantee and adds them to the subscription's group. Entitlements available immediately. - If
granteeis omitted, the group is created empty — manage membership after checkout viaPOST /api/groups/{groupId}/grantees(add,remove,replaceoperations). - Solo user — pass
granteesame asowner. Team — omitgrantee, manage group post-checkout.
Safe Update Pattern
When modifying existing plans: fetch with plans_get, copy all existing id values into the payload, change only intended fields, submit via plans_save, re-fetch and compare counts.
Plan Template (Hybrid)
{
"name": "Business",
"productId": "prod_...",
"isActive": true,
"entitlements": ["ent_id_1", "ent_id_2"],
"lineItems": [
{
"name": "base_fee", "slug": "base_fee",
"priceType": "flat_rate", "intervalType": "recurring", "billingScheme": "flat_rate",
"minQuantity": 1, "maxQuantity": 1, "tiersMode": null,
"prices": [{ "defaultCurrency": "USD", "interval": "month", "intervalCount": 1, "currencyOptions": [{ "currency": "USD", "unitAmount": 99.00 }] }]
},
{
"name": "seat", "slug": "seat",
"priceType": "per_seat", "intervalType": "recurring", "billingScheme": "per_unit",
"minQuantity": 1, "maxQuantity": 500, "allowChangingQuantity": true, "tiersMode": null,
"prices": [{ "defaultCurrency": "USD", "interval": "month", "intervalCount": 1, "currencyOptions": [{ "currency": "USD", "unitAmount": 12.00 }] }]
},
{
"name": "api_usage", "slug": "api_usage",
"priceType": "metered", "intervalType": "recurring", "billingScheme": "tiered",
"tiersMode": "graduated", "meterSlug": "api_usage", "minQuantity": 1, "maxQuantity": 1,
"prices": [{ "defaultCurrency": "USD", "interval": "month", "intervalCount": 1, "currencyOptions": [{ "currency": "USD", "unitAmount": null, "tiers": [{ "upTo": "10000", "flatAmount": null, "unitAmount": 0.02 }, { "upTo": "inf", "flatAmount": null, "unitAmount": 0.01 }] }] }]
}
]
}
For simpler plans, use only the relevant line item types. Add multiple prices entries per line item for multi-cadence.
Guardrails
Next.js Fetch Caching
Next.js App Router caches fetch() by default. Salable API calls MUST disable caching:
cache: 'no-store'on everyfetch()to Salable.export const dynamic = 'force-dynamic'on every API route handler calling Salable.
Without both, entitlement checks cached before a subscription keep returning empty forever.
Pricing and Values
unitAmountis decimal major units (dollars, not cents). $29.00 =29, never2900. Min non-null:0.01.- Line item slug:
^[a-z0-9_]+$. - One
per_seatline item max per plan. - Send
lineItemsas objects, not JSON strings. - Omit
includeArchivedby default. Only sendincludeArchived=truewhen needed. - Keep default currency consistent across plans in the same product.
- Do not mix line items with different default currencies in the same cart.
- Provide tiers for every currency option when
billingSchemeistiered. - Ensure product exists and is not archived before
plans_save. - Subscription created only after successful checkout. Updating catalog prices does not migrate existing subscriptions.
- Identity fields reject emails:
owner(cart body) andgrantee(cart-item body) — different API calls. - Entitlement key is
value, NOTname.
Locked Feature UX Policy
- When a feature is locked by permissions/capabilities, keep the control visible and disabled; do not hide it by default.
- Always explain the lock reason with both a persistent inline note and a hover/focus tooltip.
- Tooltip copy should be short and specific:
Upgrade to unlock this feature.orYou do not have access to this feature. - For disabled buttons, place the tooltip on a non-disabled wrapper so hover/focus still works.
- For locked sections, reduce visual emphasis (muted styling) but keep labels and current values readable.
- For locked views/routes, show a clear unlock message and primary CTA to the upgrade/access path; avoid dead-end screens.
- Keep internal ids/keys out of primary user-facing copy by default; use friendly names in UX text.
- Enforce the same rules server-side. UI locks are guidance; backend authorization is the source of truth.
Known API/MCP Quirks (Observed)
GET /api/plansandmcp__salable__plans_listtreatincludeArchived=falseas invalid in some environments. Omit the parameter for active-plan reads; useincludeArchived=trueonly when archived plans are explicitly requested.mcp__salable__plans_savecan fail withExpected object, received stringwhenlineItemsare serialized as strings; send object arrays.POST /api/carts/{id}/checkoutcan return400ifcancelUrlis not provided; send bothcancelUrlandsuccessUrlby default.POST /api/carts,POST /api/cart-items, and related identity fields can fail validation whenowner/granteevalues are email-formatted strings.- If application login works but runtime billing endpoints return
Unauthenticated, suspect Salable API credential/config issues first (SALABLE_API_KEYplaceholder/invalid, wrongSALABLE_API_BASE_URL) before debugging session auth. - Persisted
currencyOptions.unitAmountvalues may appear in minor units in responses even when plan-write payloads are provided as major decimal units; always re-read and validate displayed prices explicitly. subscription.ownerIdcan be an internal owner reference; owner checks for user-initiated actions should use owner-scoped list queries (owner+id) rather than direct equality with application principal id.
References
For additional detail, read these reference files in the plugin's references/ directory:
references/mcp-tool-playbook.md— operation-by-operation MCP guidance.references/pricing-model-templates.md— ready-to-adapt payload patterns.references/openapi-focus.md— API endpoints that map to MCP operations.references/auth-options.md— stack/language authentication recommendations and exit-response wording.