E-commerce checkout
Pin the cart total and merchant address. Let the customer just confirm and pay.
The most common Whisk integration. Your app already knows the cart total and where the money should go; the widget is just there to collect a signature.
The shape
"use client";
import { WhiskSend } from "@usewhisk/react";
import { useRouter } from "next/navigation";
import { useOrder } from "@/lib/orders";
export default function CheckoutPage({
params,
}: {
params: { orderId: string };
}) {
const router = useRouter();
const order = useOrder(params.orderId);
if (!order) return null;
return (
<main className="mx-auto max-w-md py-16">
<h1 className="text-2xl font-semibold mb-2">Pay {order.amount} USDC</h1>
<p className="text-muted-foreground mb-6">
Order #{order.id} · {order.summary}
</p>
<WhiskSend
amount={order.amount}
recipient={process.env.NEXT_PUBLIC_MERCHANT_TREASURY!}
destinationChain="Base"
onSuccess={({ finalTxHash }) => {
fetch(`/api/orders/${order.id}/paid`, {
method: "POST",
body: JSON.stringify({ tx: finalTxHash }),
});
router.push(`/order/confirmed?tx=${finalTxHash}`);
}}
onError={(e) => toast.error(e.message)}
/>
</main>
);
}What's locked, what's free
- Locked:
amount(the cart total),recipient(your treasury),destinationChain(where you accept USDC). - Free: the source chain. Customers send from whichever chain has their USDC; Whisk handles the bridge.
That mix is what makes this surface feel like a checkout. The user isn't typing numbers or picking recipients; they're confirming what your app has already decided.
The server-side bit you can't skip
Don't trust the finalTxHash you get from onSuccess. Anyone can
fire that callback by inspecting the page. Before you mark the order
as paid, verify the transaction on-chain from the server:
import { createPublicClient, http } from "viem";
import { base } from "viem/chains";
const client = createPublicClient({ chain: base, transport: http() });
export async function POST(req, { params }) {
const { tx } = await req.json();
const receipt = await client.getTransactionReceipt({ hash: tx });
if (!receipt || receipt.status !== "success") {
return new Response("Tx not landed", { status: 400 });
}
// Parse the USDC Transfer event from receipt.logs
// and confirm amount + recipient match the order.
await markOrderPaid(params.id, tx);
return Response.json({ ok: true });
}This step matters. Skipping it is how stores get drained.
Full source
examples/ecommerce-checkout
has the full Next.js app: order pages, server verification, the
post-payment redirect, and the Postgres schema for orders.
