简单版本
直接告诉你的 AI 助手:
Read https://docs.waffo.ai/llms-full.txt, load the official Waffo Pancake skill from https://docs.waffo.ai/zh/integrate/skill, and integrate Waffo Pancake payments into the current project.
就这一句。把这个页面作为 AI 集成主入口,需要精确 SKILL.md 时再直接打开下面的官方 Skill 入口。
完整版本
需要端到端完整集成和测试:
Read https://docs.waffo.ai/llms-full.txt, load the official Waffo Pancake skill from https://docs.waffo.ai/zh/integrate/skill, and use Waffo Pancake SDK to
integrate Waffo Pancake payments into the current project and run through
the full checkout flow:
1. Get Merchant ID from Dashboard → Merchant → Integration(将此值用作 `WAFFO_MERCHANT_ID`,不是 `storeId`)
2. Create an API Key from Dashboard → Merchant → Integration → API Keys
3. Use only `WAFFO_MERCHANT_ID` and `WAFFO_PRIVATE_KEY` as required env vars for the first working integration
4. Install @waffo/pancake-ts SDK
5. Create checkout and webhook endpoints
6. Test with card 4576750000000110
7. Verify webhook receives order.completed event
Use test environment.
官方 Waffo Pancake Skill 这是 AI 集成页内的官方 Skill 入口。打开后可查看、复制或下载团队当前实际使用的标准 SKILL.md 文件。
@waffo/pancake-ts 是 Waffo Pancake 官方服务端 TypeScript SDK,负责请求签名、收银台会话创建、Webhook 验证和 GraphQL 查询。
AI 编码工作流
当你已经明确业务模式,但希望代理把它整理成一套清晰的 Waffo 产品结构和实施方案时,AI 编码助手最有价值。
典型场景:
将官网定价页映射成 Waffo 的产品、订阅产品组和动态定价流程
判断哪些收费应建模为订阅,哪些应保留为一次性收费
设计基于 priceSnapshot 的动态定价方案
批量生成产品命名、元数据、上线清单
审查现有产品目录,找出命名、分组和生产发布上的问题
推荐工作流
先描述业务模型
说明你卖什么、如何收费、哪些部分是固定价格、哪些部分按用量或报价动态计算。
让代理输出产品规划
让它把你的收费模型映射成一次性产品、订阅产品、产品组和动态定价流程。
人工审核
确认产品命名、计费周期、商品类别,以及哪些项目应该保持为一次性收费。
落地执行
用输出结果在控制台中创建产品,或进一步生成 SDK / API 集成代码。
推荐提示词
1. 把定价页转成 Waffo 产品结构
Read https://docs.waffo.ai/llms-full.txt.
请把下面的定价模型整理成 Waffo Pancake 的产品结构:
- Starter:$19/月
- Pro:$59/月
- Scale:按年定制报价
- 超额用量:每额外 credit 收费 $0.20
- Onboarding Fee:一次性 $499
请输出:
1. 哪些应该建成订阅产品
2. 哪些应该建成一次性产品
3. 哪些订阅产品应该放进同一个产品组
4. 哪些流程需要用 priceSnapshot 做动态定价
5. 推荐的产品命名、商品类别和测试到生产的发布顺序
2. 规划动态定价
Read https://docs.waffo.ai/llms-full.txt.
我已经在 Waffo Pancake 里做了订阅业务,现在要增加超额计费。
请帮我设计:
1. 应该先创建什么基础一次性产品
2. 什么时候使用 priceSnapshot
3. 服务端在创建结账前需要计算哪些数据
4. 如何向团队解释“动态定价”和“订阅计费”的边界
3. 审查现有产品目录
Read https://docs.waffo.ai/llms-full.txt.
请审查我现有的 Waffo 产品目录,并告诉我:
1. 哪些命名不清晰
2. 哪些订阅产品应该组成产品组
3. 哪些场景更适合动态定价,而不是固定价格
4. 哪些收费虽然属于订阅业务,但仍应保留为一次性收费
5. 生产环境应该先发布哪些产品
实务判断规则
场景 推荐建模 固定公开价格 直接写在产品价格里 运行时计算金额 创建 Checkout Session 时传入 priceSnapshot 周期性收费方案 订阅产品 多个订阅套餐 每个套餐一个订阅产品,再用产品组组织 开通费、充值包 一次性产品 订阅客户的超额收费 一次性产品 + 动态定价
“订阅型业务”同时包含订阅产品和一次性收费,是完全正常的。建模要服从收费事件,而不是服从公司标签。
不建议这样做
不要把私钥或生产环境密钥直接贴进提示词
不要在未经人工审核的情况下让 AI 直接发布生产产品
不要把所有价格变化都建成新产品;如果金额在运行时计算,应优先考虑 priceSnapshot
不要把超额计费硬塞进订阅产品;如果它是事件型收费,就应该是一笔一次性收费
常见陷阱 — 先读这里
这些是导致集成失败的常见错误。写代码之前先过一遍。
陷阱 为什么会出问题 解决方法 以 JSON 方式读取 Webhook body request.json() 会重新序列化 body,改变空白字符。签名验证对比的是原始字节 。始终使用 request.text()(App Router / Hono)或 express.raw()(Express)。 使用 localtunnel 做 Webhook 隧道 localtunnel 会丢弃自定义 HTTP header。X-Waffo-Signature 根本到不了你的处理程序。 改用 cloudflared tunnel --url http://localhost:3000。 忘记调用 .publish() 产品默认创建在 test 环境。为未发布产品创建生产收银台会话会静默失败。 上线前调用 client.onetimeProducts.publish({ id }) 或 client.subscriptionProducts.publish({ id })。 访问 result 而非 result.data(GraphQL) GraphQL 客户端返回 { data: T | null, errors?: [...] }。字段嵌套在 .data 下。 始终解构:const stores = result.data?.stores ?? []。 GraphQL 变量使用 $id: ID! 后端使用 $id: String!,不是 $id: ID!。类型错误会静默返回 null。 ID 变量始终声明为 String!。 产品组更新中的 productIds 是完整替换 调用 subscriptionProductGroups.update({ productIds: [...] }) 会替换整个列表,而不是追加。 始终传入完整的目标列表,而不是只传新增项。
使用场景
Waffo Pancake 是一个 Merchant of Record(MoR)支付平台。以下项目类型适合使用 SDK:
SaaS 订阅计费 — 月付/年付计划,支持升降级(例如:Free/Pro/Team 层级)
数字商品销售 — 电子书、模板、课程、许可证的一次性购买
按次付费 — 按下载、API 调用或生成报告等操作收费
混合模式 — 订阅 + 一次性购买组合
项目类型 付费模式 需要创建的产品 AI Skills 市场 按次下载 + Pro 订阅 1 个一次性产品($0.99/次下载)+ 2 个订阅产品(月付/年付) 在线课程平台 按课程一次性购买 每门课程 1 个一次性产品(29 – 29– 29– 199) SaaS 工具(Starter/Pro/Enterprise) 订阅层级 3 个订阅产品 + 1 个产品组用于计划切换 设计模板商店 按模板一次性购买 每个模板 1 个一次性产品,或 1 个共享产品配合 priceSnapshot 覆盖价格 API 服务(用量额度) 额度包 + 订阅 每个额度包 1 个一次性产品 + 订阅提供月度配额
安装与配置
npm install @waffo/pancake-ts
仅限服务端使用。Node.js 18+。零依赖。
import { WaffoPancake } from "@waffo/pancake-ts" ;
const client = new WaffoPancake ({
merchantId: process . env . WAFFO_MERCHANT_ID ! ,
privateKey: process . env . WAFFO_PRIVATE_KEY ! ,
});
需要两个环境变量 — 注册时提供:
WAFFO_MERCHANT_ID=<你的商户ID>
WAFFO_PRIVATE_KEY=<你的RSA私钥>
WAFFO_MERCHANT_ID 指的是 Merchant ID(商户 ID) ,不是 storeId,也不是 URL 里的商店标识。storeId 仍然是当前 API 模型的一部分,在商店、商品和结账管理流程里会继续使用,不要把这两个概念混在一起。
对第一版能跑通的接入来说,只需要这两个环境变量:WAFFO_MERCHANT_ID 和 WAFFO_PRIVATE_KEY。Store ID 和 Product ID 属于运行时值,可以放在代码、应用配置或你自己的数据库里。
PEM 私钥处理
转义换行符 (最简单):
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"
Base64 编码 (推荐用于 CI/CD):
cat private.pem | base64 | tr -d '\n'
const privateKey = Buffer . from ( process . env . WAFFO_PRIVATE_KEY_BASE64 ! , "base64" ). toString ( "utf-8" );
文件路径 (本地开发):
import { readFileSync } from "fs" ;
const privateKey = readFileSync ( "./keys/private.pem" , "utf-8" );
快速开始:路径 A
// 1. Create a store
const { store } = await client . stores . create ({ name: "My SaaS" });
// 2. Create products
const { product : monthly } = await client . subscriptionProducts . create ({
storeId: store . id ,
name: "Pro Monthly" ,
billingPeriod: "monthly" ,
prices: { USD: { amount: "9.99" , taxIncluded: true , taxCategory: "saas" } },
});
const { product : yearly } = await client . subscriptionProducts . create ({
storeId: store . id ,
name: "Pro Yearly" ,
billingPeriod: "yearly" ,
prices: { USD: { amount: "99.00" , taxIncluded: true , taxCategory: "saas" } },
});
// 3. Create a checkout session
const session = await client . checkout . createSession ({
storeId: store . id ,
productId: monthly . id ,
productType: "subscription" ,
currency: "USD" ,
});
// Redirect customer to session.checkoutUrl
Store ID 和 Product ID 属于后续运行时配置。放在你应用自己的配置层即可,不必强制做成环境变量。
快速开始:路径 B
如果产品已在控制台中创建,复制产品 ID 后即可直接创建收银台会话。在这个流程里,你仍然只需要前面的两个环境变量:
const session = await client . checkout . createSession ({
productId: "PROD_xxx_from_dashboard" ,
productType: "subscription" ,
currency: "USD" ,
buyerEmail: "customer@example.com" ,
successUrl: "https://myapp.com/welcome" ,
});
// Redirect customer to session.checkoutUrl
API 参考
// Create
const { store } = await client . stores . create ({ name: "My Store" });
// Update (partial — only provided fields change)
const { store } = await client . stores . update ({
id: "store_id" ,
name: "New Name" ,
supportEmail: "help@example.com" ,
website: "https://example.com" ,
});
// Soft-delete
const { store } = await client . stores . delete ({ id: "store_id" });
一次性产品
const { product } = await client . onetimeProducts . create ({
storeId: "store_id" ,
name: "E-Book" ,
description: "A great e-book" ,
prices: {
USD: { amount: 29.00 , taxIncluded: false , taxCategory: "digital_goods" },
},
successUrl: "https://example.com/thanks" ,
metadata: { sku: "EB-001" },
});
// Update (creates new immutable version)
const { product } = await client . onetimeProducts . update ({
id: "product_id" ,
name: "E-Book v2" ,
prices: { USD: { amount: 39.00 , taxIncluded: false , taxCategory: "digital_goods" } },
});
// Publish test → production (required before going live)
const { product } = await client . onetimeProducts . publish ({ id: "product_id" });
// Activate / deactivate
const { product } = await client . onetimeProducts . updateStatus ({
id: "product_id" ,
status: "inactive" , // or "active"
});
taxCategory 选项: digital_goods | saas | software | ebook | online_course | consulting | professional_service
订阅产品
const { product } = await client . subscriptionProducts . create ({
storeId: "store_id" ,
name: "Pro Monthly" ,
billingPeriod: "monthly" , // "weekly" | "monthly" | "quarterly" | "yearly"
prices: { USD: { amount: 9.99 , taxIncluded: true , taxCategory: "saas" } },
});
// update, publish, updateStatus — same pattern as one-time products
订阅产品组
产品组支持共享试用期和订阅计划之间的切换。
const { group } = await client . subscriptionProductGroups . create ({
storeId: "store_id" ,
name: "Pro Plans" ,
rules: { sharedTrial: true },
productIds: [ "monthly_product_id" , "yearly_product_id" ],
});
// Update (productIds is FULL REPLACEMENT, not append)
await client . subscriptionProductGroups . update ({
id: "group_id" ,
productIds: [ "monthly_id" , "quarterly_id" , "yearly_id" ],
});
// Publish to production
await client . subscriptionProductGroups . publish ({ id: "group_id" });
// Delete (physical delete, not soft-delete)
await client . subscriptionProductGroups . delete ({ id: "group_id" });
收银台会话
const session = await client . checkout . createSession ({
storeId: "store_id" , // optional if productId is from an existing store
productId: "product_id" ,
productType: "onetime" , // "onetime" | "subscription"
currency: "USD" ,
buyerEmail: "buyer@example.com" , // optional, pre-fills email
successUrl: "https://example.com/thanks" , // redirect after payment
metadata: { orderId: "internal-123" }, // custom key-value pairs
// Optional overrides:
priceSnapshot: { amount: 19.99 , taxIncluded: true , taxCategory: "saas" },
billingDetail: { country: "US" , isBusiness: false },
expiresInSeconds: 3600 , // default: 7 days
});
// session.checkoutUrl — redirect customer here
// session.sessionId — for tracking
// session.expiresAt — ISO 8601 expiry
订单级参数与优先级
通过 createSession 传递的参数可以覆盖产品级设置。理解优先级关系对 AI 集成至关重要:
参数 作用 优先级 priceSnapshot覆盖产品价格(动态定价场景) 最高 — 忽略产品设定价格currency指定结账货币 必填 — 从产品 prices 中选取对应币种 buyerEmail预填消费者邮箱 可选 — 跳过结账页邮箱输入步骤 billingDetail预填账单信息(国家、税号等) 可选 — 跳过结账页地址输入 successUrl支付成功跳转地址 覆盖产品级 successUrl metadata自定义键值对(内部订单号等) 透传到 webhook event.data withTrial是否启用试用期 覆盖产品级试用设置 expiresInSeconds会话过期时间 默认 45 分钟,最长 7 天 darkMode结账页暗色模式 true=暗色 / false=亮色 / 省略=门店默认
priceSnapshot 是动态定价的核心参数。 当传入 priceSnapshot 时,产品上设定的价格会被完全忽略。这适用于:按用量阶梯定价、优惠码动态折扣、A/B 测试不同价格点等场景。
// 动态定价示例:根据用量阶梯覆盖价格
const session = await client . checkout . createSession ({
storeId: "store_id" ,
productId: "api-credits-product-id" ,
productType: "onetime" ,
currency: "USD" ,
priceSnapshot: { amount: 49.00 , taxIncluded: true , taxCategory: "saas" }, // 覆盖产品价格
buyerEmail: "user@example.com" ,
metadata: { internalOrderId: "ORD-2024-001" , tier: "growth" },
successUrl: "https://myapp.com/purchase/success" ,
});
Webhook 集成要点
集成 Webhook 时需要注意以下关键点:
要点 说明 原始 body 必须用 request.text() 读取,不能用 .json() — 签名验证依赖原始字节 幂等处理 用 event.id(投递 ID)去重,同一事件可能重试多次 立即返回 200 先返回 200 OK,再异步处理业务逻辑。超时会触发重试 环境区分 event.mode 为 "test" 或 "prod",确保测试事件不触发生产逻辑重试机制 非 2xx 或超时触发重试(默认 3 次,指数退避)
典型 Webhook 处理模式:
import { verifyWebhook } from "@waffo/pancake-ts" ;
export async function POST ( request : Request ) {
const body = await request . text ();
const sig = request . headers . get ( "x-waffo-signature" );
try {
const event = verifyWebhook ( body , sig );
// 幂等检查
if ( await isDuplicate ( event . id )) return new Response ( "OK" );
await markProcessed ( event . id );
// 按事件类型分发
switch ( event . eventType ) {
case "order.completed" :
await handleOrderCompleted ( event . data );
break ;
case "subscription.activated" :
await handleSubscriptionActivated ( event . data );
break ;
case "subscription.canceled" :
await handleSubscriptionCanceled ( event . data );
break ;
case "subscription.past_due" :
await handlePastDue ( event . data );
break ;
}
return new Response ( "OK" );
} catch {
return new Response ( "Invalid signature" , { status: 401 });
}
}
取消订阅
const { orderId , status } = await client . orders . cancelSubscription ({
orderId: "order_id" ,
});
// status: "canceled" (was pending) or "canceling" (active → ends at period end)
GraphQL 查询
只读查询。ID 变量使用 String!(不是 ID!)。
const result = await client . graphql . query <{
stores : Array <{ id : string ; name : string ; status : string }>;
}>({
query: `query { stores { id name status } }` ,
});
const stores = result . data ?. stores ?? [];
// With variables — note String!, not ID!
const result = await client . graphql . query <{
onetimeProduct : { id : string ; name : string ; prices : unknown };
}>({
query: `query ($id: String!) { onetimeProduct(id: $id) { id name prices } }` ,
variables: { id: "product_id" },
});
const product = result . data ?. onetimeProduct ;
Webhook 验证
SDK 内置 test 和 prod 两个环境的公钥。验证只需一次函数调用。
Next.js App Router
import { verifyWebhook } from "@waffo/pancake-ts" ;
export async function POST ( request : Request ) {
const body = await request . text (); // MUST be raw text, not .json()
const sig = request . headers . get ( "x-waffo-signature" );
try {
const event = verifyWebhook ( body , sig );
// event.eventType, event.data, event.storeId, event.mode
return new Response ( "OK" );
} catch {
return new Response ( "Invalid signature" , { status: 401 });
}
}
Express
import express from "express" ;
import { verifyWebhook } from "@waffo/pancake-ts" ;
// MUST use express.raw(), not express.json()
app . post ( "/webhooks" , express . raw ({ type: "application/json" }), ( req , res ) => {
try {
const event = verifyWebhook (
req . body . toString ( "utf-8" ),
req . headers [ "x-waffo-signature" ] as string ,
);
res . status ( 200 ). send ( "OK" );
} catch {
res . status ( 401 ). send ( "Invalid signature" );
}
});
Hono
import { verifyWebhook } from "@waffo/pancake-ts" ;
app . post ( "/webhooks" , async ( c ) => {
const body = await c . req . text (); // raw text
const sig = c . req . header ( "x-waffo-signature" );
try {
const event = verifyWebhook ( body , sig );
return c . text ( "OK" );
} catch {
return c . text ( "Invalid signature" , 401 );
}
});
验证选项
// Explicit environment (skip auto-detection)
const event = verifyWebhook ( body , sig , { environment: "prod" });
// Custom replay tolerance (default: 5 minutes)
const event = verifyWebhook ( body , sig , { toleranceMs: 600000 });
// Disable replay protection (not recommended in production)
const event = verifyWebhook ( body , sig , { toleranceMs: 0 });
事件类型
事件 触发条件 order.completed一次性支付成功 subscription.activated首次订阅支付成功 subscription.payment_succeeded续费支付成功 subscription.canceling发起取消(当前周期结束前仍有效) subscription.uncanceled撤回取消 subscription.updated计划变更(升级/降级) subscription.canceled订阅完全终止 subscription.past_due续费支付失败 refund.succeeded退款完成 refund.failed退款失败
事件结构
interface WebhookEvent {
id : string ; // Delivery ID (use for idempotent dedup)
timestamp : string ; // ISO 8601 UTC
eventType : string ; // e.g. "order.completed"
eventId : string ; // Business event ID (payment/order ID)
storeId : string ;
mode : "test" | "prod" ;
data : {
orderId : string ;
buyerEmail : string ;
currency : string ;
amount : number ; // 美元金额(如 9.99)
taxAmount : number ;
productName : string ;
};
}
配置 Webhook URL
await client . stores . update ({
id: "store_id" ,
webhookSettings: {
testWebhookUrl: "https://your-domain.com/api/webhooks" ,
prodWebhookUrl: "https://your-domain.com/api/webhooks" ,
testEvents: [
"order.completed" ,
"subscription.activated" ,
"subscription.canceled" ,
"subscription.past_due" ,
],
prodEvents: [
"order.completed" ,
"subscription.activated" ,
"subscription.canceled" ,
"subscription.past_due" ,
],
},
});
错误处理
import { WaffoPancakeError } from "@waffo/pancake-ts" ;
try {
await client . stores . create ({ name: "My Store" });
} catch ( err ) {
if ( err instanceof WaffoPancakeError ) {
console . log ( err . status ); // HTTP status code
console . log ( err . errors ); // Array of { message, layer }
console . log ( err . errors [ 0 ]. layer ); // "store" | "product" | "order" | ...
}
}
错误按调用栈深度排序:errors[0] 是根本原因(最深层),errors[n] 是最外层调用者。
开发提示
Webhook 隧道 — 使用 cloudflared,不要用 localtunnel(见陷阱表)。
brew install cloudflare/cloudflare/cloudflared
cloudflared tunnel --url http://localhost:3000
幂等性自动处理 — SDK 根据 merchantId + path + body 自动生成确定性键。重试是安全的。
Test → Prod 工作流 — 产品默认创建在 test 环境。使用 .publish() 发布到生产环境。Webhook 事件包含 mode: "test" | "prod",你的处理程序可以据此区分。
快速上手清单
npm install @waffo/pancake-ts
配置 WAFFO_MERCHANT_ID 和 WAFFO_PRIVATE_KEY 环境变量
初始化 new WaffoPancake({ merchantId, privateKey })
创建或引用商店
创建或引用产品
创建收银台:client.checkout.createSession(...) → 重定向到 checkoutUrl
处理 Webhook:verifyWebhook(rawBody, sig) — 必须使用 request.text()
配置 Webhook URL:client.stores.update({ webhookSettings: ... })