From salable
Monetize apps on beta.salable.app 2.0 using Salable MCP tools and REST API. Use when creating or modifying products, plans, line items, prices, currency options, tiers, and entitlements, including packaging decisions like flat-rate, per-seat, and metered pricing. When asked to monetize or monetise an app, assume scope includes in-app paywall pricing tables, feature gating with entitlements, and subscription management views; determine entitlement mapping with the user before provisioning.
How this skill is triggered — by the user, by Claude, or both
Slash command
/salable:salable-monetizeThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
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).
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).
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 SALABLE_API_KEY="your_key" (add to ~/.zshrc for persistence).SALABLE_API_KEY is set in your environment./salable:salable.Do not proceed without a working MCP connection.
The openapi.yaml is truncated in browsers/WebFetch but the full spec is available via curl. At session start, download it:
curl -s https://beta.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 beta.salable.app contracts only, not docs.salable.app.
When asked to "monetize/monetise this app", deliver three things:
Determine entitlement definitions and plan-to-feature mapping with the user before writing catalog resources.
Run before any catalog writes or app integration:
owner, grantee, granteeId). Prefer: org/tenant id > stable internal user id > non-email user.id. Last resort: derive usr_${hmac_sha256(email, salt)}.email), not identity ids.owner must be the app's external principal, never Salable internal ids like owner_....Before any provisioning, gather all of the following from the user. Do not assume or skip items.
export_pdf, team_collab)per_seat per plan.)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
curl -s https://beta.salable.app/openapi.yaml > /tmp/salable-openapi.yamlplans_save per plan -> verify with plans_get./tmp/salable-openapi.yaml for exact request/response schemas of every endpoint you will call.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.
Salable API utility — Server-side helper (salableApi) with cache: '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, and refresh(). 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=success redirect from Stripe with a success banner and entitlement refresh. Wrap useSearchParams() 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 .env and .env.example with all required Salable env vars including provisioned plan IDs.
After building, run next build to verify compilation. Fix any errors (Suspense boundaries, missing imports, type errors) before presenting the result to the user.
| 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.
GET /api/plans, GET /api/productsGET /api/entitlements/checkGET /api/subscriptions, GET /api/subscriptions/{id}, GET /api/subscriptions/{id}/invoicesPOST /api/subscriptions/{id}/portalPUT /api/subscriptions/{id}/itemsPOST /api/subscriptions/{id}/cancel, PUT /api/subscriptions/{id}/auto-renewAll REST responses are wrapped in a data envelope. Always unwrap through .data:
{ "type": "object", "data": { ...payload } } — access body.data.{ "type": "list", "data": [ ...items ], "hasMore": bool } — access body.data (the array).Common mistake: body.entitlements instead of body.data.entitlements, or body.url instead of body.data.url.
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.
GET /api/entitlements/checkQuery 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.
POST /api/subscriptions/{id}/portalCreates 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"
}
Entitlement access chain: Grantee -> Membership -> Group -> Subscription Plans -> Entitlements.
grantee is passed on the cart-item, Salable auto-creates the grantee and adds them to the subscription's group. Entitlements available immediately.grantee is omitted, the group is created empty — manage membership after checkout via POST /api/groups/{groupId}/grantees (add, remove, replace operations).grantee same as owner. Team — omit grantee, manage group post-checkout.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.
{
"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.
Next.js App Router caches fetch() by default. Salable API calls MUST disable caching:
cache: 'no-store' on every fetch() 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.
unitAmount is decimal major units (dollars, not cents). $29.00 = 29, never 2900. Min non-null: 0.01.^[a-z0-9_]+$.per_seat line item max per plan.lineItems as objects, not JSON strings.includeArchived by default. Only send includeArchived=true when needed.billingScheme is tiered.plans_save.owner (cart body) and grantee (cart-item body) — different API calls.value, NOT name.Upgrade to unlock this feature. or You do not have access to this feature.GET /api/plans and mcp__salable__plans_list treat includeArchived=false as invalid in some environments. Omit the parameter for active-plan reads; use includeArchived=true only when archived plans are explicitly requested.mcp__salable__plans_save can fail with Expected object, received string when lineItems are serialized as strings; send object arrays.POST /api/carts/{id}/checkout can return 400 if cancelUrl is not provided; send both cancelUrl and successUrl by default.POST /api/carts, POST /api/cart-items, and related identity fields can fail validation when owner/grantee values are email-formatted strings.Unauthenticated, suspect Salable API credential/config issues first (SALABLE_API_KEY placeholder/invalid, wrong SALABLE_API_BASE_URL) before debugging session auth.currencyOptions.unitAmount values 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.ownerId can 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.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.npx claudepluginhub salable/salable-claude-code-plugin --plugin salableManages Clerk Billing subscription lifecycle: render PricingTable, gate with has(), configure plans/features, handle webhooks. Use for SaaS monetization, plan gating, checkout flows, trials, invoicing.
Clerk Billing and Stripe subscription management setup. Use when implementing subscriptions, configuring pricing plans, setting up billing, adding payment flows, managing entitlements, or when user mentions Clerk Billing, Stripe integration, subscription management, pricing tables, payment processing, or monetization.
Implements SaaS monetization with Stripe: subscriptions, freemium, pricing experiments, upgrade flows, churn prevention, revenue optimization, and business models.