Follow these steps to build an Onchain Ticketing app with Coinbase’s OnchainKit, allowing wallet connection, secure checkout, and integration with Airtable for inventory management.
Fork This Project
For a quick start, you can fork the complete project here to get all the files and configurations in one place. This includes the advanced implementation, so you can modify it to suit your specific needs.
Beginner: OnchainKit Setup & Simple Ticket Purchase
Create a ticketing app with wallet connection and crypto checkout.
1. Setup the Project
Install OnchainKit by creating a new project with:
npx create-onchain@latest
You’ll see these files:
providers.tsx
: Configures OnchainKit providers.
page.tsx
: Main ticketing page and UI.
.env
: Stores API keys.
Add your OnchainKit API Key in .env
:
NEXT_PUBLIC_ONCHAINKIT_CDP_KEY=YOUR_API_KEY
2. Wallet UI & Ticket Product Setup
- Create a ticket product in Coinbase Commerce and save the
productId
.
- Edit
page.tsx
:
- Import wallet and checkout components.
- Replace
"YOUR_PRODUCT_ID"
with your actual product ID.
import { ConnectWallet, Wallet } from "@coinbase/onchainkit/wallet";
import { Checkout, CheckoutButton, CheckoutStatus } from "@coinbase/onchainkit/checkout";
export default function TicketPage() {
return (
<div>
<Wallet>
<ConnectWallet />
</Wallet>
<Checkout productId="YOUR_PRODUCT_ID">
<CheckoutButton />
<CheckoutStatus />
</Checkout>
</div>
);
}
Add user information fields, metadata, and a confirmation page for a two-step checkout.
In page.tsx
, add inputs for name, email, and ticket count:
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [ticketCount, setTicketCount] = useState(1);
const ticketPrice = 0.5;
const totalAmount = (ticketCount * ticketPrice).toFixed(2);
2. Confirmation Page
Add a new folder app/confirmation with page.tsx to review order details before payment. Use useSearchParams
to get user data:
const searchParams = useSearchParams();
const name = searchParams.get("name");
const email = searchParams.get("email");
const ticketCount = searchParams.get("ticketCount");
3. Backend Setup for Secure Charge Creation
Create a secure API endpoint to handle charge creation. In app/api/createCharge/route.ts
:
import { NextResponse } from "next/server";
export async function POST(req) {
const { amount, metadata } = await req.json();
const response = await fetch("https://api.commerce.coinbase.com/charges", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CC-Api-Key": process.env.COINBASE_COMMERCE_API_KEY || "",
},
body: JSON.stringify({
local_price: { amount, currency: "USDC" },
pricing_type: "fixed_price",
metadata,
}),
});
const data = await response.json();
return NextResponse.json(
data.data?.id ? { chargeId: data.data.id } : { error: "Failed to create charge" },
);
}
4. Connecting the Backend to Frontend
In confirmation/page.tsx
, add a handler to call the backend:
const handleCreateCharge = async () => {
const response = await fetch("/api/createCharge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: totalAmount, metadata: { name, email, ticketCount } }),
});
const data = await response.json();
return data.chargeId;
};
Advanced: Inventory Management with Airtable
Integrate Airtable to manage ticket inventory and dynamically display available tickets.
1. Airtable Setup
- Create a Tickets Base with fields:
Name
: Ticket name
Price
: Price per ticket
Inventory
: Available quantity of tickets
Image
: Event or ticket image
Description
: Brief description of the ticket or event
- API Keys: Get your Airtable API Key, Base ID, and Table ID. Save them in
.env.local
:
AIRTABLE_API_KEY=your_airtable_api_key
AIRTABLE_BASE_ID=your_airtable_base_id
AIRTABLE_TABLE_ID=your_airtable_table_id
Fetch Tickets from Airtable
Create app/api/fetchTickets/route.ts
:
import { NextResponse } from "next/server";
export async function GET() {
const response = await fetch(
`https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/${process.env.AIRTABLE_TABLE_ID}`,
{
headers: { Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}` },
},
);
const data = await response.json();
const tickets = data.records.map((record) => ({
id: record.id,
name: record.fields.Name,
price: record.fields.Price,
inventory: record.fields.Inventory || 0,
imageUrl: record.fields.Image[0]?.url || "",
description: record.fields.Description || "",
}));
return NextResponse.json({ tickets });
}
3. Update Inventory after Purchase
Create app/api/updateInventory/route.ts
to reduce inventory:
export async function POST(request) {
const { ticketId, quantity } = await request.json();
const url = `https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/${process.env.AIRTABLE_TABLE_ID}/${ticketId}`;
const response = await fetch(url, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ fields: { Inventory: quantity } }),
});
return response.ok
? NextResponse.json({ success: true })
: NextResponse.json({ error: "Failed to update inventory" });
}
Display Tickets with Inventory in page.tsx
Fetch tickets in useEffect
, displaying available tickets and marking sold-out items:
useEffect(() => {
async function fetchTickets() {
const response = await fetch("/api/fetchTickets");
const data = await response.json();
setTickets(data.tickets);
}
fetchTickets();
}, []);
When the user confirms, navigate to the confirmation page with all relevant data:
const handleConfirm = () => {
router.push(
`/confirmation?ticketId=${ticketId}&name=${ticketName}&price=${totalAmount}&userName=${userName}&email=${email}&ticketCount=${ticketCount}`,
);
};
5. Confirmation & Payment
In confirmation/page.tsx
, after successful payment, reduce inventory:
const handleCreateCharge = async () => {
const response = await fetch("/api/createCharge", {
/* amount and metadata */
});
const data = await response.json();
if (data.chargeId) {
await fetch("/api/updateInventory", {
method: "POST",
body: JSON.stringify({ ticketId, quantity: ticketCount }),
});
}
};
With this setup, you’ve added dynamic inventory management, limiting ticket quantities based on availability and updating inventory post-purchase. This final step makes your Onchain Ticketing app fully operational and ready for real-world use!