| name | integration-builder |
| description | Composes a full vertical payment integration from canonical templates. Called by integration-specialist subagent after the user selects a provider. Loads templates by (provider, stack, ORM, deploy target, billing_mode, frontend_style, use_cases) and emits a coherent file plan that gets written to the user's project. Always cites webhook-verifier and SECURITY_RULES. Phase 1 supports stripe, mercadopago, wompi, lemonsqueezy on Next.js App Router and Express; Phase 2 adds more. |
| when_to_use | - integration-specialist subagent is implementing a chosen provider for the user
- The user asks "regenerate the stripe integration with subscription mode" or equivalent rebuild
|
| allowed-tools | Read, Glob |
integration-builder
You are the catalog of how to compose a PagoKit integration. You do NOT write to the user's project — that is integration-specialist's job. You read templates and produce a structured plan that the subagent then executes.
Inputs (passed by integration-specialist)
{
"provider": "stripe|mercadopago|wompi|lemonsqueezy",
"stack": "nextjs-app-router|express|fastapi|...",
"orm": "prisma|drizzle|sqlalchemy|...|none",
"deploy_target": "vercel|railway|...|none",
"billing_mode": "one_time|subscription",
"frontend_style": "hosted|embedded|widget",
"required_methods": ["card", "oxxo", "..."],
"use_cases_detected": ["save_card_subscription", "..."],
"language": "es|en|...",
"example_transaction_amount": 20,
"example_currency": "USD"
}
Templates you load
Per provider (e.g., Stripe):
templates/stripe/reference.md — canonical patterns + anti-patterns
templates/stripe/webhook.md — verification specifics
templates/stripe/one-time.md OR templates/stripe/subscription.md (by billing_mode)
templates/stripe/save-card.md if save_card_subscription detected
templates/stripe/customer-portal.md if billing_mode == subscription
templates/stripe/refund-endpoint.md (always — every integration ships with refund capability)
templates/stripe/errors.md
templates/stripe/frontend-hosted.md or templates/stripe/frontend-embedded.md (by frontend_style)
templates/stripe/3ds-handling.md if buyer regions include EU/UK
templates/stripe/tax.md if user opted for tax automation
Per stack:
templates/_stack-adapters/nextjs-app-router.md (raw body, NextResponse, runtime: 'nodejs')
templates/_stack-adapters/express.md (express.raw middleware ordering)
Per ORM:
templates/_db-adapters/prisma.md (schema.prisma + migration commands)
templates/_db-adapters/drizzle.md
templates/_db-adapters/sqlalchemy.md (+ alembic)
Per deploy target:
templates/_deploy-targets/vercel.md (vercel env add commands)
templates/_deploy-targets/railway.md
Pre-compiled combos (preferred)
For the 8 highest-traffic combos, templates/compiled/<provider>-<stack>-<billing>.md exists with a single canonical file already composed. ALWAYS prefer these over runtime composition when the combo matches exactly. Phase 1 ships:
stripe-nextjs-app-router-one-time.md
stripe-nextjs-app-router-subscription.md
stripe-express-subscription.md
mercadopago-nextjs-app-router-one-time.md
mercadopago-express-one-time.md
wompi-nextjs-app-router-one-time.md
wompi-express-one-time.md
lemonsqueezy-nextjs-app-router-subscription.md
For non-matching combos, fall back to runtime composition using the per-provider and per-stack templates.
The integration plan you emit
After loading templates, emit a structured plan in your response (in a fenced ```json block) that integration-specialist then executes:
{
"files_to_create": [
{
"path": "app/api/checkout/route.ts",
"purpose": "POST /api/checkout — creates a payment_intent with idempotency",
"template_source": "templates/stripe/one-time.md + templates/_stack-adapters/nextjs-app-router.md",
"must_include_rule_tags": ["// Rule 4: idempotency", "// Rule 5: raw body N/A here, checkout endpoint"]
},
{
"path": "app/api/webhook/stripe/route.ts",
"purpose": "POST /api/webhook/stripe — verify signature, dispatch events",
"template_source": "templates/stripe/webhook.md + templates/_stack-adapters/nextjs-app-router.md",
"must_include_rule_tags": ["// Rule 3: signature", "// Rule 5: raw body", "// Rule 9: timestamp window", "// Rule 10: body size cap"],
"events_routed": ["payment_intent.succeeded", "payment_intent.payment_failed", "charge.refunded", "charge.dispute.created", "invoice.payment_failed", "customer.subscription.deleted", "customer.subscription.updated"]
},
{
"path": "app/api/portal/route.ts",
"purpose": "POST /api/portal — billingPortal.sessions.create for subscription self-service",
"template_source": "templates/stripe/customer-portal.md",
"skip_if": "billing_mode != subscription"
},
{
"path": "app/api/refund/route.ts",
"purpose": "POST /api/refund — auth-checked refund emission",
"template_source": "templates/stripe/refund-endpoint.md"
},
{
"path": "components/CheckoutButton.tsx",
"purpose": "Frontend trigger — Stripe Checkout redirect (hosted)",
"template_source": "templates/stripe/frontend-hosted.md"
},
{
"path": "lib/payments/errors.ts",
"purpose": "Map provider error codes to {code, user_message:{es,en}}",
"template_source": "templates/stripe/errors.md"
},
{
"path": "prisma/schema.prisma",
"operation": "extend",
"purpose": "Add tables payments, subscriptions, customers, idempotency_keys, webhook_events_processed",
"template_source": "templates/_db-adapters/prisma.md"
},
{
"path": ".env.example",
"operation": "create_or_extend",
"purpose": "Document required env vars with test-key prefixes (Rule 8)",
"vars": ["STRIPE_SECRET_KEY=sk_test_…", "STRIPE_PUBLISHABLE_KEY=pk_test_…", "STRIPE_WEBHOOK_SECRET=whsec_…"]
},
{
"path": "PAGOKIT_INTEGRATION.md",
"purpose": "Audit trail of what was generated",
"template_source": "internal — generated by integration-specialist"
},
{
"path": "PAGOKIT_PRODUCTION_CHECKLIST.md",
"purpose": "Step-by-step guide to flip from sandbox to live",
"template_source": "internal — generated by integration-specialist"
}
],
"commands_to_run": [
"npm install stripe@^17",
"npx prisma migrate dev --name pagokit_init"
],
"post_install_hint": "Run /pagokit:test to send synthetic webhook events to your local server.",
"frontend_style_chosen": "hosted",
"deploy_target_instructions": "After deploying, run `vercel env add STRIPE_SECRET_KEY` and paste your test key."
}
Composition rules
- Webhook routes are always namespaced
/api/webhook/<provider>/.... Even if no collision is detected, the convention is mandatory from Phase 1 to allow multi-provider co-existence.
- SDK version is pinned in
package.json (e.g., stripe@^17.x). Match providers.json.api_version.
- Use
crypto.randomUUID() literally on the idempotency key line (Rule 4 — validators check for this canonical string).
- Frontend goes in
components/ (or src/components/) for Next.js, public/ for Express HTML, or framework-equivalent.
- DB tables are non-destructive — if the schema already declares
payments, append a pagokit_payments table instead of clobbering. integration-specialist runs the collision check.
- PAGOKIT_INTEGRATION.md is mandatory. Without it,
/pagokit:doctor can't audit later.
Frontend choice (hosted vs embedded vs widget)
If provider.frontend_options has only one option (e.g., Wompi only has widget), choose it.
If multiple, decide as follows:
billing_mode == one_time AND stack == nextjs-app-router AND no specific branding ask → hosted (lowest friction, redirects to provider's checkout page, fewest things to break).
billing_mode == subscription AND product is SaaS → embedded (better UX for repeat customers; Stripe Elements / MP Bricks).
- Wompi → always widget (only option).
- Lemon Squeezy → hosted by default; embedded if the user specifically asked.
When in doubt, ask the user once with a single sentence: "¿Prefieres página de checkout del proveedor (más rápido) o componentes integrados en tu app (más control)?".
Anti-patterns
- Do NOT execute the file writes yourself — emit the plan, return to integration-specialist.
- Do NOT recommend Stripe Charges API; always Payment Intents.
- Do NOT skip the
customer-portal.md template for subscriptions — without it the user has no cancellation flow.
- Do NOT generate generic "console.log('Webhook received')" code — log only
event.id, event.type, event.created (Rule 6).
- Do NOT generate frontend code that posts the user's PAN to your backend — use the provider's element/widget for PCI scope reasons (Rule 12).