Create the Paddle products and prices that other Paddle skills depend on — try MCP tools first, fall back to a Node SDK seed script, or dictate dashboard steps as a last resort.
62
72%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/catalog-setup/SKILL.mdThe user is starting a Paddle integration and their account doesn't yet have the products and prices that other Paddle skills (checkout-web, pricing-pages, sandbox-testing, webhooks) depend on. Use this skill to create them.
This is typically a one-time setup per project. Once the catalog exists, you don't revisit this skill — only when the user wants to add new products or prices.
Before creating anything, agree on these with the user. Do not guess.
saas for software-as-a-service or standard for everything else, and surface the choice after creating for the user to confirm. Users must get approval for other tax categories in the dashboard.month / year) — billing_cycle in MCP body params, billingCycle in the SDK; one-time prices omit it. Trial periods are subscription-only — the API rejects them on one-time prices.unit_price.amount in MCP, unitPrice.amount in the SDK) is a string in lowest currency units. Cents for USD/EUR/GBP ("1000" = $10.00). Whole units for the zero-decimal currencies — JPY, KRW, and CLP ("1200" = ¥1,200 / ₩1,200 / CLP$1,200, not ¥12 etc.). CLP has historical centavos but Paddle treats it as zero-decimal for everyday transactions.pro_... and pri_... IDs from one don't exist in the other. Ask the user before targeting production.IDs look like pro_01h... (products) and pri_01h... (prices). You'll need the price IDs to wire into the user's app, so capture them after creation.
| Category | Use for |
|---|---|
saas | SaaS subscriptions, web apps, platform-as-a-service |
digital-goods | Downloadable software, apps, themes, plugins, asset packs |
ebooks | Ebooks, magazines, paid newsletters |
standard | Anything else (default tax treatment per jurisdiction) |
website-hosting | Hosting plans, domain registration |
professional-services | Consulting, custom dev, advisory work |
training-services | Online courses, workshops, certification programs |
implementation-services | Setup, integration, migration services |
Pick the highest-ranked method that's available to you:
search and execute tools from a paddle-sandbox (or paddle-live) MCP server are in your toolset, use them. Fastest, no code for the user to run, results immediate. Always prefer this.PADDLE_API_KEY (or can generate one), generate a seed script for the user to run. Repeatable, version-controlled.If a method fails midway (e.g. SDK call returns forbidden because the API key lacks permissions), fall back to the next method rather than retrying.
If the user doesn't have a Paddle MCP server installed, surface it as a suggestion — point them at the Paddle MCP server install guide.
Check whether search and execute tools from paddle-sandbox (or paddle-live) are available to you. If yes, use them.
The Paddle MCP exposes three tools per server (
search,execute,report_missing_tool). Workflow: callsearchto confirm the exact method name and parameter shapes, then callexecutewith an async function that callsclient.<resource>.<operation>(...). Method paths are camelCase (client.clientTokens.create,client.pricingPreview.preview). Body params and response fields are snake_case (tax_category,product_id,unit_price,currency_code). Pagination is{ pagination: { hasMore }, data: [...] }with{ after: "<last_id>" }— not.next()/.hasMore. Chain multi-step workflows inside oneexecute; variables don't persist between calls. Hard caps: 50 API calls per execute, 30s timeout, 32KB code.
Default to paddle-sandbox unless the user has explicitly asked to target live.
search with a query like "products create" and "prices create" to confirm the current method names and parameter shapes.execute with one async function that creates the product and all its prices in a single call. Chaining inside one execute avoids round-trips and keeps related operations atomic.Example execute payload — one product, monthly + yearly prices, monthly with a trial:
async (client) => {
const product = await client.products.create({
name: "Pro",
tax_category: "saas",
description: "For scaling teams.",
});
const monthly = await client.prices.create({
product_id: product.id,
description: "Pro monthly USD",
unit_price: { amount: "1000", currency_code: "USD" },
billing_cycle: { interval: "month", frequency: 1 },
trial_period: { interval: "day", frequency: 14 },
});
const yearly = await client.prices.create({
product_id: product.id,
description: "Pro yearly USD",
unit_price: { amount: "9600", currency_code: "USD" },
billing_cycle: { interval: "year", frequency: 1 },
});
return { product_id: product.id, monthly_id: monthly.id, yearly_id: yearly.id };
}For a one-time price (lifetime license, etc.), drop billing_cycle and trial_period:
const lifetime = await client.prices.create({
product_id: product.id,
description: "Pro lifetime",
unit_price: { amount: "29900", currency_code: "USD" },
});For regional pricing, add unit_price_overrides — all snake_case, including the inner field names:
unit_price_overrides: [
{ country_codes: ["DE", "FR", "IT", "ES", "NL"], unit_price: { amount: "900", currency_code: "EUR" } },
{ country_codes: ["GB"], unit_price: { amount: "800", currency_code: "GBP" } },
{ country_codes: ["JP"], unit_price: { amount: "1200", currency_code: "JPY" } },
]If the MCP search / execute tools aren't available, fall back to Method 2.
Use this when the user has (or can install) @paddle/paddle-node-sdk and a PADDLE_API_KEY. You write the script; the user runs it.
PADDLE_API_KEY set in their environment. If not, point them to Paddle > Developer tools > Authentication in the sandbox dashboard and ask them to create one with at minimum the product.write and price.write permission scopes.@paddle/paddle-node-sdk to the project (npm install @paddle/paddle-node-sdk), if not present.Template:
// scripts/seed-paddle-catalog.ts
import { Environment, Paddle } from "@paddle/paddle-node-sdk";
const paddle = new Paddle(process.env.PADDLE_API_KEY!, {
environment: Environment.sandbox, // change to .production for live
});
async function seed() {
const pro = await paddle.products.create({
name: "Pro",
taxCategory: "saas",
description: "For scaling teams.",
});
const monthly = await paddle.prices.create({
productId: pro.id,
description: "Pro monthly USD",
unitPrice: { amount: "1000", currencyCode: "USD" }, // 1000 cents = $10.00
billingCycle: { interval: "month", frequency: 1 },
trialPeriod: { interval: "day", frequency: 14 },
});
const yearly = await paddle.prices.create({
productId: pro.id,
description: "Pro yearly USD",
unitPrice: { amount: "9600", currencyCode: "USD" },
billingCycle: { interval: "year", frequency: 1 },
});
console.log(
JSON.stringify({ productId: pro.id, monthlyId: monthly.id, yearlyId: yearly.id }, null, 2),
);
}
seed().catch((e) => {
console.error(e);
process.exit(1);
});Run it: npx tsx scripts/seed-paddle-catalog.ts.
One-time price (lifetime license, etc.) — drop billingCycle and trialPeriod:
await paddle.prices.create({
productId: pro.id,
description: "Pro lifetime",
unitPrice: { amount: "29900", currencyCode: "USD" },
});Regional overrides — add unitPriceOverrides:
unitPriceOverrides: [
{ countryCodes: ["DE", "FR", "IT", "ES", "NL"], unitPrice: { amount: "900", currencyCode: "EUR" } },
{ countryCodes: ["GB"], unitPrice: { amount: "800", currencyCode: "GBP" } },
{ countryCodes: ["JP"], unitPrice: { amount: "1200", currencyCode: "JPY" } },
],If the script fails with forbidden or unauthorized, the API key lacks the required permission scopes — ask the user to regenerate it with product.write and price.write. If it fails because the user can't or won't install the SDK, fall back to Method 3.
Last resort. Use when MCP and SDK paths are both blocked. Dictate the steps for the user to follow, then ask them to share the IDs.
Tell the user:
<the value you agreed on above>. Cannot be changed after a sale.10.00 USD).pri_... IDs from the price details panel and paste them back to me.After the user shares the IDs, update their app config (constants file, env vars, or wherever you've decided price IDs live).
Whichever method you used, before declaring this skill complete:
execute call: client.products.list({ include: ["prices"] }). Via SDK: paddle.products.list({ include: "prices" }). Or ask the user to check Paddle > Catalog > Products in the dashboard.client.products.get(productId, { include: ["prices"] }) for each new product (note productId is a positional path param). Via SDK: paddle.products.get(id). Check name + tax category.checkout-web next, using one of the new price IDs, to confirm the catalog actually works in checkout.paddle-sandbox + a pdl_sdbx_... key for sandbox, paddle-live + a live key for production. With the SDK, Environment.sandbox vs Environment.production. Always confirm before creating.unit_price.amount (MCP) / unitPrice.amount (SDK) is a string in lowest currency units. "1000" is $10.00, not $1000. The zero-decimal currencies (JPY, KRW, CLP) are whole units — "1200" is ¥1,200 / ₩1,200 / CLP$1,200, not ¥12 etc.client.prices.create() (MCP) and paddle.prices.create() (SDK) both require the parent product's ID — product_id in MCP body params, productId in SDK. Create the product first, capture its id, then pass to each price.trial_period (MCP) / trialPeriod (SDK) if there's no billing cycle.billing_cycle (MCP) / billingCycle (SDK), the price is created as one-time. Always set { interval: "month", frequency: 1 } (or similar) for subscriptions.execute code. Method paths are camelCase (client.clientTokens.create, client.pricingPreview.preview), but body params and response fields are snake_case (tax_category, product_id, unit_price, currency_code). The SDK is camelCase end-to-end — don't carry that convention into execute code, and don't carry the snake_case convention back into SDK code.execute invocations. Variables don't persist between execute calls, so passing IDs requires returning them and re-passing on the next call. Chain dependent operations (product → prices → overrides) inside one async function and return the IDs at the end.client.products.create() returns forbidden, the key lacks product.write / price.write — ask the user to regenerate it from Paddle > Developer tools > Authentication with those scopes added.62438cd
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.