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>
  );
}

Intermediate: Metadata & Multi-Step Flow

Add user information fields, metadata, and a confirmation page for a two-step checkout.

1. Add User Input Fields & Metadata

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!