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
78%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./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 downloadable software, and surface the choice after creating for the user to confirm. Other categories need approval first — see below.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.400 bad_request with the offending field in errors.
trial_period: { interval, frequency }. The customer adds a payment method at checkout and pays nothing until the trial ends.trial_period.unit_price ({ amount, currency_code }) for the trial charge. Its currency must match the base price. If the price has unit_price_overrides, add a trial_period.unit_price_overrides entry with the same country_codes for every one. Use the MCP (Method 1) or the API directly — the Node SDK doesn't support trial pricing yet.trial_period.requires_payment_method: false, so the customer can start without a payment method. Cardless trials are in early access; without access, the API returns 400 bad_request. A trial can't be both paid and cardless.description is required and internal — customers never see it. name (optional, 1–150 characters) is shown to customers at checkout and on invoices, e.g. "Monthly". quantity limits how many can be bought at this price and defaults to 1–100; set quantity: { minimum, maximum } if you need a different range, e.g. for seats.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, VND, and CLP ("1200" = ¥1,200 / ₩1,200 / CLP$1,200, not ¥12 etc.). To check any currency, read decimal_places from client.currencies.list() (MCP; returns a plain array) or the public GET /currencies endpoint.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 | Approval needed? |
|---|---|---|
saas | Products that allow users to connect to and use online or cloud-based applications over the Internet. | No |
standard | Software products that are pre-written and can be downloaded and installed onto a local device. | No |
digital-goods | Non-customizable digital files or media (not software) acquired with an up front payment that can be accessed without any physical product being delivered. | Yes |
ebooks | Digital books and educational material which is sold with permanent rights for use by the customer. | Yes |
implementation-services | Remote configuration, set-up, and integrating software on behalf of a customer. | Yes |
professional-services | Services that involve the application of your expertise and specialized knowledge of a software product. | Yes |
software-programming-services | Services that can be used to customize and white label software products. | Yes |
training-services | Training and education services related to software products. | Yes |
website-hosting | Cloud storage service for personal or corporate information, assets, or intellectual property. | Yes |
Only saas and standard are available without approval. The user requests other categories under Paddle > My account > Settings > Taxable categories in their live account. Creating a product with a category that isn't approved returns product_tax_category_not_approved.
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). List methods return{ pagination: { hasMore, estimatedTotal }, <resource>: [...] }, notdata. The array's key is the resource's API path segment, so it's kebab-case where the method path is camelCase:(await client.products.list()).products,(await client.clientTokens.list())["client-tokens"]. Page with{ after: "<last_id>" }whilepagination.hasMoreis true, not.next(). Lists that aren't paginated, likeclient.currencies.list(), return a plain array. Single-entity methods return the entity. Chain multi-step workflows inside oneexecute; variables don't persist between calls. Hard caps: 50 API calls per execute, 30s timeout, and 32,000 characters of 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",
name: "Monthly",
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",
name: "Yearly",
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 a paid trial, add unit_price to trial_period (plus unit_price_overrides if the price has overrides — see "Trial type" above):
trial_period: {
interval: "day",
frequency: 14,
unit_price: { amount: "100", currency_code: "USD" }, // $1.00 for the 14-day trial
},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 > My account > Settings > Authentication in the sandbox dashboard and ask them to create one on the API keys tab with at minimum the product.write and price.write permissions.@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",
name: "Monthly",
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",
name: "Yearly",
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" } },
],Paid trials — the Node SDK's trialPeriod type has no unitPrice yet, and price responses drop it. Create paid-trial prices with the MCP (Method 1) or a direct POST /prices call.
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>.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, { include: ["prices"] }). Check name + tax category.checkout-web next, using one of the new price IDs, to confirm the catalog actually works in checkout.paddle-sandbox for sandbox, paddle-live for production. paddle-sandbox authenticates with a pdl_sdbx_... API key; paddle-live authorizes with OAuth in the browser (or a live API key). 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, VND, 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 on paddle-sandbox, the API key lacks product.write / price.write — ask the user to regenerate it from Paddle > My account > Settings > Authentication with those permissions added. On paddle-live connected with OAuth, the connection starts read-only, so a write returning forbidden is expected until the user grants write permissions under Paddle > Connectors > MCPs. Permissions there are capped by their Paddle user's role.PaddleHQ/paddle-agent-skills
70cb70d
Also appears in
since Oct 1, 2026
since Sep 19, 2026
since Sep 7, 2026
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.