2d-games
無料2D game development principles. Sprites, tilemaps, physics, camera.
日本語の概要は準備中です。原文の説明を表示しています。
Integrate MercadoPago Checkout Pro (redirect-based) into Next.js applications with any PostgreSQL database (Supabase, AWS RDS, Neon, PlanetScale, self-hosted, Prisma, Drizzle, or raw pg). Use when the user needs to: (1) Add MercadoPago payment processing to a Next.js app, (2) Create a checkout flow with MercadoPago, (3) Set up payment webhooks for MercadoPago, (4) Build payment success/failure pages, (5) Create a shopping cart with payment integration, (6) Troubleshoot MercadoPago integration issues (auto_return errors, webhook failures, hydration mismatches, double submissions). Triggers on requests mentioning MercadoPago, Mercado Pago, payment integration with MP, Argentine/Latin American payment processing, or checkout with MercadoPago. Supports all MercadoPago countries: Argentina (ARS), Brazil (BRL), Mexico (MXN), Colombia (COP), Chile (CLP), Peru (PEN), Uruguay (UYU).
インストールする前に、エージェントに与えられる指示の中身を確認できます。
Redirect-based payment flow: buyer clicks "Pay", is redirected to MercadoPago, completes payment, returns to the app. A webhook confirms the payment status in the background.
For a minimal integration, just tell Claude:
Integrar MercadoPago en mi app
Claude will automatically explore your codebase to detect:
For more control, provide details:
Integrate MercadoPago Checkout Pro.
Database: Prisma. Currency: ARS. Success route: /pago-exitoso.
See references/usage-examples.md for more prompt templates.
┌─────────────────────────────────────────────────────────────────────────────┐
│ PAYMENT FLOW │
└─────────────────────────────────────────────────────────────────────────────┘
User clicks "Pay"
│
▼
┌──────────────────┐
│ POST /api/checkout│
└────────┬─────────┘
│
▼
┌──────────────────────────────────┐
│ 1. Create purchase in DB │
│ (status: pending) │
│ 2. Create preference in MP API │
│ 3. Save preference_id in DB │
│ 4. Return init_point URL │
└────────┬─────────────────────────┘
│
▼
┌──────────────────┐ ┌─────────────────────┐
│ Redirect to MP │─────▶│ User pays on MP │
└──────────────────┘ └──────────┬──────────┘
│
┌───────────────────────────┴───────────────────────────┐
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Redirect back to app │ │ MP sends webhook │
│ /payment-success?id=... │ │ POST /api/webhooks/mp │
└──────────┬──────────────┘ └──────────┬──────────────┘
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────┐
│ Verify status via API │ │ Update purchase status │
│ GET /api/purchases/[id] │ │ in database │
└──────────┬──────────────┘ └─────────────────────────┘
│
▼
┌─────────────────────────┐
│ Show UI based on status │
│ approved/pending/rejected│
└─────────────────────────┘
Determine the database adapter. Explore the codebase or ask the user:
references/database-supabase.mdreferences/database-prisma.mdreferences/database-postgresql.mdGather or infer from the codebase:
| Detail | Why | Example |
|---|---|---|
| Currency | Preference creation | ARS, BRL, MXN (see references/countries.md) |
| Success/failure routes | back_urls in preference | /payment-success, /pago-exitoso |
| Brand name | Card statement descriptor | MY_STORE (max 22 chars) |
| Product/item table | FK in purchase_items | products, photos, courses |
| Cart store location | Hook reads items from it | src/store/cart.ts |
| DB client path | API routes import it | src/lib/supabase/server.ts, src/lib/prisma.ts |
npm install mercadopago zodNEXT_PUBLIC_):
MERCADOPAGO_ACCESS_TOKEN=TEST-xxxx # from https://www.mercadopago.com/developers/panel/app
NEXT_PUBLIC_APP_URL=http://localhost:3000 # HTTPS in production
assets/migration.sql (works on any PostgreSQL database).auto_return and secure webhooks/api/webhooks/mercadopagoCreate: src/lib/db/purchases.ts
This abstracts all purchase DB operations. Implement using your DB adapter. See the reference file for your adapter:
references/database-supabase.mdreferences/database-prisma.mdreferences/database-postgresql.mdThe helper must export these functions:
interface PurchaseInsert {
user_email: string;
status: 'pending';
total_amount: number;
}
interface PurchaseUpdate {
status?: 'pending' | 'approved' | 'rejected';
mercadopago_payment_id?: string;
mercadopago_preference_id?: string;
user_email?: string;
updated_at?: string;
}
// Required exports:
export async function createPurchase(data: PurchaseInsert): Promise<{ id: string }>;
export async function updatePurchase(id: string, data: PurchaseUpdate): Promise<void>;
export async function getPurchaseStatus(id: string): Promise<{ id: string; status: string } | null>;
export async function createPurchaseItems(purchaseId: string, items: { item_id: string; price: number }[]): Promise<void>;
Create: src/lib/mercadopago/client.ts
import { MercadoPagoConfig, Preference, Payment } from 'mercadopago';
const client = new MercadoPagoConfig({
accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN!,
});
const preference = new Preference(client);
const payment = new Payment(client);
interface CreatePreferenceParams {
items: { id: string; title: string; quantity: number; unit_price: number }[];
purchaseId: string;
buyerEmail?: string;
}
export async function createPreference({
items, purchaseId, buyerEmail,
}: CreatePreferenceParams) {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
return preference.create({
body: {
items: items.map((item) => ({
id: item.id,
title: item.title,
quantity: item.quantity,
unit_price: item.unit_price,
currency_id: 'ARS', // Change per references/countries.md
})),
...(buyerEmail ? { payer: { email: buyerEmail } } : {}),
back_urls: {
success: `${baseUrl}/payment-success?purchase=${purchaseId}`,
failure: `${baseUrl}/payment-failure?purchase=${purchaseId}`,
pending: `${baseUrl}/payment-success?purchase=${purchaseId}&status=pending`,
},
// CRITICAL: auto_return requires HTTPS. Omit on localhost or MP returns 400.
...(baseUrl.startsWith('https') ? { auto_return: 'approved' as const } : {}),
external_reference: purchaseId,
notification_url: `${baseUrl}/api/webhooks/mercadopago`,
statement_descriptor: 'YOUR_BRAND', // Replace with user's brand (max 22 chars)
expires: true,
expiration_date_from: new Date().toISOString(),
expiration_date_to: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
},
// Optional: Prevent duplicate preferences on retry
requestOptions: {
idempotencyKey: purchaseId,
},
});
}
export async function getPayment(paymentId: string) {
return payment.get({ id: paymentId });
}
Create: src/app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import { createPurchase, updatePurchase } from '@/lib/db/purchases';
import { createPreference } from '@/lib/mercadopago/client';
import { z } from 'zod';
const checkoutSchema = z.object({
items: z.array(z.object({
id: z.string(),
title: z.string().min(1),
quantity: z.number().positive(),
unit_price: z.number().positive(),
})).min(1),
email: z.string().email().optional(),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const validation = checkoutSchema.safeParse(body);
if (!validation.success) {
return NextResponse.json({ error: 'Invalid request data' }, { status: 400 });
}
const { items, email } = validation.data;
const totalAmount = items.reduce((sum, i) => sum + i.unit_price * i.quantity, 0);
const purchase = await createPurchase({
user_email: email || 'pending@checkout',
status: 'pending',
total_amount: totalAmount,
});
const mpPreference = await createPreference({
items, purchaseId: purchase.id, buyerEmail: email,
});
await updatePurchase(purchase.id, {
mercadopago_preference_id: mpPreference.id,
});
return NextResponse.json({
preferenceId: mpPreference.id,
initPoint: mpPreference.init_point,
purchaseId: purchase.id,
});
} catch (error) {
console.error('Checkout error:', error);
return NextResponse.json({ error: 'Checkout failed' }, { status: 500 });
}
}
Create: src/app/api/webhooks/mercadopago/route.ts
import { NextResponse } from 'next/server';
import { getPurchaseStatus, updatePurchase } from '@/lib/db/purchases';
import { getPayment } from '@/lib/mercadopago/client';
export async function POST(request: Request) {
try {
const body = await request.json();
// Handle both IPN and webhook formats
if (body.type !== 'payment' && body.action !== 'payment.created' && body.action !== 'payment.updated') {
return NextResponse.json({ received: true });
}
const paymentId = body.data?.id;
if (!paymentId) return NextResponse.json({ received: true });
const payment = await getPayment(paymentId.toString());
if (!payment?.external_reference) return NextResponse.json({ received: true });
let status: 'pending' | 'approved' | 'rejected' = 'pending';
if (payment.status === 'approved') status = 'approved';
else if (['rejected', 'cancelled', 'refunded'].includes(payment.status || '')) status = 'rejected';
// Idempotency: skip if already in terminal state
const existing = await getPurchaseStatus(payment.external_reference);
if (existing?.status === 'approved' || existing?.status === 'rejected') {
return NextResponse.json({ received: true });
}
const payerEmail = payment.payer?.email;
await updatePurchase(payment.external_reference, {
status,
mercadopago_payment_id: paymentId.toString(),
...(payerEmail ? { user_email: payerEmail } : {}),
updated_at: new Date().toISOString(),
});
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
// Always return 200 to prevent MercadoPago from retrying indefinitely
return NextResponse.json({ received: true });
}
}
// GET endpoint for MercadoPago verification pings
export async function GET() {
return NextResponse.json({ status: 'ok' });
}
Create: src/app/api/purchases/[id]/route.ts
import { NextResponse } from 'next/server';
import { getPurchaseStatus } from '@/lib/db/purchases';
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const data = await getPurchaseStatus(id);
if (!data) return NextResponse.json({ error: 'Not found' }, { status: 404 });
return NextResponse.json({ id: data.id, status: data.status });
}
Create: src/hooks/useCheckout.ts
Double-click prevention uses useRef (survives re-renders, unlike useState).
'use client';
import { useCallback, useRef, useState } from 'react';
export function useCheckout() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const guard = useRef(false);
const submitCheckout = useCallback(async (items: unknown[]) => {
if (guard.current) return;
setError(null);
guard.current = true;
setIsSubmitting(true);
try {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Checkout failed');
if (data.initPoint) window.location.href = data.initPoint;
else throw new Error('No payment link returned');
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
setIsSubmitting(false);
guard.current = false;
}
}, []);
return { submitCheckout, isSubmitting, error };
}
Create: src/app/payment-success/page.tsx (adjust route name)
Always verify purchase status server-side. Never trust the redirect URL alone.
Wrap useSearchParams in <Suspense> (Next.js App Router requirement).
'use client';
import { useSearchParams } from 'next/navigation';
import { Suspense, useCallback, useEffect, useState } from 'react';
type Status = 'loading' | 'approved' | 'pending' | 'rejected' | 'error';
function PaymentResult() {
const purchaseId = useSearchParams().get('purchase');
const [status, setStatus] = useState<Status>(purchaseId ? 'loading' : 'approved');
const verify = useCallback(async (id: string) => {
try {
const res = await fetch(`/api/purchases/${id}`);
if (!res.ok) { setStatus('error'); return; }
const { status } = await res.json();
setStatus(status === 'approved' ? 'approved'
: status === 'pending' ? 'pending' : 'rejected');
} catch { setStatus('error'); }
}, []);
useEffect(() => { if (purchaseId) verify(purchaseId); }, [purchaseId, verify]);
if (status === 'loading') {
return <div>Verifying payment...</div>;
}
if (status === 'approved') {
return (
<div>
<h1>Payment Successful!</h1>
<p>Thank you for your purchase.</p>
</div>
);
}
if (status === 'pending') {
return (
<div>
<h1>Payment Pending</h1>
<p>Your payment is being processed.</p>
<p>You'll receive an email when confirmed.</p>
<p>This may take 1-3 business days for offline payment methods.</p>
</div>
);
}
if (status === 'rejected') {
return (
<div>
<h1>Payment Failed</h1>
<p>Your payment could not be processed.</p>
<p>Please try again or use a different payment method.</p>
</div>
);
}
return (
<div>
<h1>Error</h1>
<p>Could not verify payment status. Please contact support.</p>
</div>
);
}
export default function PaymentSuccessPage() {
return <Suspense fallback={<div>Loading...</div>}><PaymentResult /></Suspense>;
}
mercadopago + zod installedMERCADOPAGO_ACCESS_TOKEN in .env (TEST token for dev, never NEXT_PUBLIC_)NEXT_PUBLIC_APP_URL in .env (HTTPS in production)purchases + purchase_items)src/lib/db/purchases.ts)createPreference and getPayment/api/checkout with Zod validation/api/webhooks/mercadopago with idempotency check/api/purchases/[id] for status verificationuseRef guard (prevents double submit)useSearchParams wrapped in <Suspense>NEXT_PUBLIC_APP_URL uses HTTPSauto_return enabled (only works with HTTPS)For detailed solutions, see references/troubleshooting.md.
| Gotcha | Fix |
|---|---|
auto_return + localhost = 400 error | Only set when URL starts with https |
user_email NOT NULL + no email = 500 | Use 'pending@checkout' placeholder; webhook updates it |
currency_id doesn't match account country | Use correct currency (ARS for Argentina, BRL for Brazil, etc.) |
| Hydration mismatch (localStorage cart) | Add mounted state guard before rendering cart content |
| Double purchase on double-click | Use useRef guard, not just useState |
| Success page trusts redirect URL | Always verify via /api/purchases/[id] |
| Webhook duplicate updates | Check if purchase is already terminal before updating |
| Webhooks can't reach localhost | Use ngrok: ngrok http 3000 |
useSearchParams error | Wrap component in <Suspense> |
| Payment stuck in pending | Normal for offline methods (OXXO, Rapipago, Boleto) |
| Mixed test/production credentials | Never mix - use all TEST or all PROD |
references/database-supabase.md - Supabase DB helper implementationreferences/database-prisma.md - Prisma DB helper implementationreferences/database-postgresql.md - Raw PostgreSQL (pg, Drizzle, etc.) DB helper implementationreferences/countries.md - Currencies, test cards, payment methods by countryreferences/testing.md - Complete testing guide with test cards and simulated resultsreferences/mcp-server.md - MercadoPago MCP Server for AI integrationreferences/troubleshooting.md - 20+ common errors and solutionsreferences/usage-examples.md - Ready-to-use prompt templatesassets/migration.sql - Database schema template (standard PostgreSQL)まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
2D game development principles. Sprites, tilemaps, physics, camera.
日本語の概要は準備中です。原文の説明を表示しています。
3D game development principles. Rendering, shaders, physics, cameras.
日本語の概要は準備中です。原文の説明を表示しています。
Expert in building 3D experiences for the web - Three.js, React Three Fiber, Spline, WebGL, and interactive 3D scenes. Covers product configurators, 3D portfolios, immersive websites, and bringing depth to web experiences. Use when: 3D website, three.js, WebGL, react three fiber, 3D experience.
日本語の概要は準備中です。原文の説明を表示しています。
Structured guide for setting up A/B tests with mandatory gates for hypothesis, metrics, and execution readiness.
日本語の概要は準備中です。原文の説明を表示しています。
This skill should be used when the user asks to "attack Active Directory", "exploit AD", "Kerberoasting", "DCSync", "pass-the-hash", "BloodHound enumeration", "Golden Ticket", "Silver Ticket", "AS-REP roasting", "NTLM relay", or needs guidance on Windows domain penetration testing.
日本語の概要は準備中です。原文の説明を表示しています。
Use when you need to address review or issue comments on an open GitHub Pull Request using the gh CLI.
日本語の概要は準備中です。原文の説明を表示しています。