# Project export: CallToChange

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 11.0
- Tagline: An LLM emissions logger powered by SUI blockchain
- Devpost: https://devpost.com/software/calltochange-ghne9o
- GitHub: https://github.com/adityakakarla/calltochange
- Demo: https://calltochange-theta.vercel.app/
- Video: https://www.youtube.com/embed/TMWhvw60K70?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Aditya Kakarla (11 commits)

## Devpost submission (written by the team)

### Inspiration

When we landed in SF, we knew we wanted to tackle climate change with LLMs. We spent a while thinking about it. But then, we realized something—LLMs are (in a sense) furthering climate change. It takes significant amounts of energy to train and run these models, especially multimodal tools.

### What it does

Users can log text and image generation calls using our Python library. They can view how many of each call they've made in our dashboard, along with CO2 emissions generated and the cost to offset them. If users choose, they can offset their emissions using Sui. The general public can also verify whether or not a company has offset their emissions through a dynamic page.

### How we built it

We built our web app with Next.js. Our frontend was supported by Tailwind CSS and shadcn/ui, while our backend utilized MongoDB to track each user's LLM calls. We used Suiet Wallet kit to enable Sui Wallet integrations into our app. We also developed and published our custom Python library using PyPI.

### Challenges we ran into

Setting up blockchain transactions (no prior experience) Connecting to a Chrome extension (no prior experience) Organizing MongoDB functions (wrote 250+ lines of helper functions in <4 hours)

### Accomplishments we're proud of

Getting everything deployed on Vercel Configuring blockchain transactions with Sui Getting the Python library to work with our backend API routes Engineering dynamic routes to give the general public transparency into carbon offsetting progress

### What we learned

How to work with Sui blockchain transactions How to write optimized functions to fetch and write data to MongoDB How to deploy a Python library That SF is an awesome city

### What's next

Building out enterprise-ready code Adding smart contract-esque functionality with Sui Developing carbon credit tokens with Sui

## README (from the GitHub repository)

# [CallToChange]([https://calltochange.vercel.app/](https://calltochange-theta.vercel.app/)) 🌍

## Team LiquidDeath: Aditya, Ganesh

---

## Description

CallToChange is a carbon emission calculator designed by Team LiquidDeath. This tool analyzes and calculates the carbon emissions generated by a company's LLM (Large Language Model) calls. The application offers a comprehensive solution for monitoring and reducing the environmental impact of LLM call operations.

---

## Inspiration

Our inspiration for CallToChange stemmed from the growing need to address carbon emissions generated by technology operations and a way to keep companies accountable and aware of the emissions they produce from AI usage. We aimed to create a tool that empowers companies to measure and mitigate their carbon footprint related to LLM calls.

---

## What it does

CallToChange seamlessly integrates with our custom Python library (call-to-change) to log LLM call emissions. It utilizes academic research and information from the Department of Energy to accurately determine the carbon footprint of each LLM call. Our visually engaging dashboards provide clear insights into emissions data, aiding companies in making informed decisions to reduce their environmental impact.

---

## How we built it

We developed CallToChange using Next.js and Python.

Our Python library logs LLM calls in MongoDB. Under the hood, our library does this by sending a POST request to API routes in our web app. These API routes securely handle data updates in MongoDB.

In our web app, we use Next.js and MongoDB to fetch user information (such as the number of times a user has used text generation). To create an interactive interface, we utilized Tailwind CSS. For user authentication, we used Clerk to manage user accounts.

---

## Screenshots

![image](https://github.com/Arush223/CallToChange/assets/115517528/cabedfe8-b9a7-4473-9759-b6e638612de0)

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.


## Detected evidence (automated analysis)

Indexed codebase: 31 recognized source files, 59 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- MongoDB (technology) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Python (language) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (38 of 38)

```
.eslintrc.json
.gitignore
app/about/page.tsx
app/actions.ts
app/api/image/route.ts
app/api/text/route.ts
app/contact/page.tsx
app/dashboard/page.tsx
app/docs/page.tsx
app/globals.css
app/layout.tsx
app/not-found.tsx
app/page.tsx
app/pay/[amount]/page.tsx
app/pay/layout.tsx
app/sign-in/[[...sign-in]]/page.tsx
app/sign-up/[[...sign-up]]/page.tsx
app/verify/[tag]/page.tsx
components.json
components/Navbar.tsx
components/ui/alert.tsx
components/ui/button.tsx
components/ui/card.tsx
components/ui/form.tsx
components/ui/input.tsx
components/ui/label.tsx
components/ui/navigation-menu.tsx
components/ValueCard.tsx
lib/utils.ts
middleware.ts
next.config.mjs
package.json
postcss.config.mjs
README.md
scripts/emissions.js
scripts/mongo.js
tailwind.config.ts
tsconfig.json
```

### Dependencies

- package.json: @clerk/nextjs@^5.7.5, @geist-ui/core@^2.3.8, @hookform/resolvers@^3.9.0, @mysten/sui@^1.13.0, @radix-ui/react-icons@^1.3.0, @radix-ui/react-label@^2.1.0, @radix-ui/react-navigation-menu@^1.2.1, @radix-ui/react-slot@^1.1.0, @suiet/wallet-kit@^0.3.3, @types/node@^20, @types/react@^18, @types/react-dom@^18, all@^0.0.0, class-variance-authority@^0.7.0, clsx@^2.1.1, dotenv@^16.4.5, eslint@^8, eslint-config-next@14.2.15, geist@^1.3.1, lucide-react@^0.453.0, mini-css-extract-plugin@^2.9.1, mongodb@^6.9.0, next@14.2.15, postcss@^8, react@^18, react-dom@^18, react-hook-form@^7.53.1, react-spinners-kit@^1.9.1, tailwind-merge@^2.5.4, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5, web3@^4.13.0, xrpl@^4.0.0, zod@^3.23.8

### Recent commits (newest first)

- updated readme and stuff
- ran build
- updated sui
- updated for sui
- metamask
- pnpm issue
- updated dashboard
- finished basic functionality
- update ui for ganesh
- initial commit
- Initial commit from Create Next App

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "chainge",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@clerk/nextjs": "^5.7.5",
    "@geist-ui/core": "^2.3.8",
    "@hookform/resolvers": "^3.9.0",
    "@mysten/sui": "^1.13.0",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-navigation-menu": "^1.2.1",
    "@radix-ui/react-slot": "^1.1.0",
    "@suiet/wallet-kit": "^0.3.3",
    "all": "^0.0.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "dotenv": "^16.4.5",
    "geist": "^1.3.1",
    "lucide-react": "^0.453.0",
    "mongodb": "^6.9.0",
    "next": "14.2.15",
    "react": "^18",
    "react-dom": "^18",
    "react-hook-form": "^7.53.1",
    "react-spinners-kit": "^1.9.1",
    "tailwind-merge": "^2.5.4",
    "tailwindcss-animate": "^1.0.7",
    "web3": "^4.13.0",
    "xrpl": "^4.0.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.15",
    "mini-css-extract-plugin": "^2.9.1",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  },
  "type": "module"
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Quicksand } from "next/font/google"; // Import Quicksand
import "./globals.css";
import { Navbar } from "@/components/Navbar";
import { ClerkProvider } from "@clerk/nextjs";
import '@suiet/wallet-kit/style.css';

const quicksand = Quicksand({
  subsets: ["latin"],
  variable: "--font-quicksand",
  weight: ['300', '400', '500', '700'], // Specify the weights you need
});

export const metadata: Metadata = {
  title: "CallToChange",
  description: "LLM Emissions Logger With BlockChain",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body
          className={`${quicksand.variable} antialiased bg-[#204544] min-h-screen`}
          style={{ fontFamily: 'var(--font-quicksand)' }}
        >
          <Navbar />
          {children}
        </body>
      </html>
    </ClerkProvider>
  );
}

```

### app/page.tsx

```typescript
import { Button } from "@/components/ui/button";
import Link from "next/link";

export default function VideoWithOverlay() {
  return (
    <div className="relative w-full h-screen overflow-hidden">
      <div className="absolute inset-0 z-0">
        <video
          autoPlay
          muted
          loop
          playsInline
          className="w-full h-full object-cover"
          poster="https://cdn.prod.website-files.com/65b8cd72835ceeacd4449a53/65c8ed3e7d491e37259a30c5_Langchain-hero-1_1706794335%201-placeholder.jpg"
        >
          <source
            src="https://customer-xp1a3vy0ydc4ega7.cloudflarestream.com/bb6cf069546e3d829aa5808ac8b07748/downloads/default.mp4"
            type="video/mp4"
          />
          Your browser does not support the video tag.
        </video>
      </div>
      <div className="absolute bottom-40 left-20 text-white text-8xl z-10 space-y-5">
        <h1>Carbon-Neutral LLM Apps.</h1>
        <h1>Built With CallToChange.</h1>
        <div className="flex flex-row space-x-5 pt-10">
        <Button className="bg-white hover:bg-green-200 transition duration-300 ease-in-out text-black rounded-full py-8 px-12 text-lg" asChild>
          <a href='https://youtu.be/TMWhvw60K70'>
          View Demo
          </a>
          </Button>
          <Button className="bg-transparent border border-white text-white transition duration-300 ease-in-out hover:bg-black hover:bg-opacity-20 rounded-full py-8 px-12 text-lg">
          <Link href='/sign-up'>
          Get Started
          </Link>
          </Button>        </div>
      </div>
    </div>
  );
}

```

### app/pay/layout.tsx

```typescript
'use client'

import { WalletProvider } from "@suiet/wallet-kit"

export default function PayLayout({
    children,
  }: {
    children: React.ReactNode
  }) {
    return (
        <WalletProvider>
        <div>
            {children}
        </div>
        </WalletProvider>
    )
  }
```

### app/contact/page.tsx

```typescript
import Link from "next/link"
import { Button } from "@/components/ui/button"

export default function Contact() {
    return (
        <div className="relative w-full h-screen overflow-hidden">
      <div className="absolute inset-0 z-0">
        <video
          autoPlay
          muted
          loop
          playsInline
          className="w-full h-full object-cover"
          poster="https://cdn.prod.website-files.com/65b8cd72835ceeacd4449a53/65c8ed3e7d491e37259a30c5_Langchain-hero-1_1706794335%201-placeholder.jpg"
        >
          <source
            src="https://customer-xp1a3vy0ydc4ega7.cloudflarestream.com/bb6cf069546e3d829aa5808ac8b07748/downloads/default.mp4"
            type="video/mp4"
          />
          Your browser does not support the video tag.
        </video>
      </div>
      <div className="absolute bottom-40 left-20 text-white text-8xl z-10 space-y-5">
        <h1>Contact Us</h1>
        <h1 className="text-2xl">We love criticism.</h1>
        <h1 className="text-2xl">Reach us at adi[at]adikakarla[dot]com</h1>
        <div className="flex flex-row space-x-5 pt-10">
          <Button className="bg-white hover:bg-green-200 transition duration-300 ease-in-out text-black rounded-full py-8 px-12 text-lg" asChild>
          <a href='https://youtu.be/TMWhvw60K70'>
          View Demo
          </a>
          </Button>
          <Button className="bg-transparent border border-white text-white transition duration-300 ease-in-out hover:bg-black hover:bg-opacity-20 rounded-full py-8 px-12 text-lg">
          <Link href='/sign-up'>
          Get Started
          </Link>
          </Button>
        </div>
      </div>
    </div>
    )
}
```

### app/about/page.tsx

```typescript
import Link from "next/link"
import { Button } from "@/components/ui/button"

export default function About() {
    return (
        <div className="relative w-full h-screen overflow-hidden">
      <div className="absolute inset-0 z-0">
        <video
          autoPlay
          muted
          loop
          playsInline
          className="w-full h-full object-cover"
          poster="https://cdn.prod.website-files.com/65b8cd72835ceeacd4449a53/65c8ed3e7d491e37259a30c5_Langchain-hero-1_1706794335%201-placeholder.jpg"
        >
          <source
            src="https://customer-xp1a3vy0ydc4ega7.cloudflarestream.com/bb6cf069546e3d829aa5808ac8b07748/downloads/default.mp4"
            type="video/mp4"
          />
          Your browser does not support the video tag.
        </video>
      </div>
      <div className="absolute bottom-40 left-20 text-white text-8xl z-10 space-y-5">
        <h1>About Us</h1>
        <h1 className="text-2xl">Built by <span>Aditya Kakarla</span> and <span>Ganesh Kumarappan</span> at Cal Hacks.</h1>
        <h1 className="text-2xl">Developed using Next.js, Ripple, Clerk, and Tailwind.</h1>
        <h1 className="text-2xl">UI inspired by LangChain.</h1>
        <div className="flex flex-row space-x-5 pt-10">
          <Button className="bg-white hover:bg-green-200 transition duration-300 ease-in-out text-black rounded-full py-8 px-12 text-lg" asChild>
          <a href='https://youtu.be/TMWhvw60K70'>
          View Demo
          </a>
          </Button>
          <Button className="bg-transparent border border-white text-white transition duration-300 ease-in-out hover:bg-black hover:bg-opacity-20 rounded-full py-8 px-12 text-lg">
          <Link href='/sign-up'>
          Get Started
          </Link>
          </Button>
        </div>
      </div>
    </div>
    )
}
```

### app/docs/page.tsx

```typescript
import { Inter } from "next/font/google";

const inter = Inter({
    subsets: ["latin"],
    variable: "--font-inter",
});

export default function Docs() {
    return (
        <div className={`${inter.variable} antialiased bg-[#204544] min-h-screen flex flex-col py-32 px-20`}
            style={{ fontFamily: 'var(--font-inter)' }}>
            <div className="bg-white rounded-lg p-10">
                <h1 className="text-[#204544] text-6xl mb-8">Docs</h1>
                <section className="mb-10">
                    <h2 className="text-4xl mb-4">Getting Started</h2>
                    <p className="text-lg mb-4">
                        To get started with our library, install it using pip:
                    </p>
                    <pre className="bg-gray-100 p-4 rounded-lg">
                        <code className="text-sm">
                            pip install call-to-change
                        </code>
                    </pre>
                </section>

                <section className="mb-10">
                    <h2 className="text-4xl mb-4">Usage Example</h2>
                    <p className="text-lg mb-4">
                        Here’s a simple example of how to use CallToChange in your project:
                    </p>
                    <pre className="bg-gray-100 p-4 rounded-lg">
                        <code className="text-sm">
                            {`from openai import OpenAI
from call_to_change import log

client = OpenAI()

log(client, 'test@test.com')

response = client.chat.completions.create(
  model="gpt-4o-mini",
  messages=[
    {"role": "system", "content": "You are a helpful assistant."},
    {"role": "user", "content": "Who won the world series in 2020?"},
  ]
)

print(response.choices[0].message.content)`}
                        </code>
                    </pre>
                </section>

                <section>
                    <h2 className="text-4xl mb-4">Behind The Scenes</h2>
                    <p className="text-lg mb-4">
                        Our library consists of multiple systems:
                    </p>
                    <ul className="list-disc list-inside mb-4">
                        <li>
                            <p>Next.js for our full-stack application</p>
                        </li>
                        <li>
                            <p>Clerk for user authentication</p>
                        </li>
                        <li>
                            <p>SUI for carbon offset transactions</p>
                        </li>
                        <li>
                            <p>SingleStore for blazingly fast data updates</p>
                        </li>
                    </ul>
                </section>
            </div>
        </div>
    );
}

```

### app/dashboard/page.tsx

```typescript
"use client";

import React, { useState, useEffect } from "react";
import { addUser, fetchUserInfo } from "../actions";
import {
  convertEmissionsToOffsetCost,
  convertTextAndImageCallsToEmissions,
} from "@/scripts/emissions";
import ValueCard from "@/components/ValueCard";
import { SignedOut } from "@clerk/nextjs";
import { MetroSpinner } from 'react-spinners-kit';
import { redirect } from "next/navigation";
import Link from "next/link";
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { useForm } from "react-hook-form"
import {zodResolver} from "@hookform/resolvers/zod"

const formSchema = z.object({
  tag: z.string().min(1, {
    message: "Tag must be at least 1 character.",
  }).regex(/^[a-zA-Z0-9-]+$/, {
    message: "Tag can only contain letters, numbers, and hyphens.",
  }),
})

export default function Page() {
  const [data, setData] = useState<{ label: string; value: number | string }[]>([]);
  const [loading, setLoading] = useState(true);
  const [isNewUser, setIsNewUser] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    const setUserInfo = async () => {
      setLoading(true);
      setError("");

      try {
        const result = await fetchUserInfo();
        if (result) {
          const { text_count, image_count, amount_offset } = result;
          const emissions = convertTextAndImageCallsToEmissions(text_count, image_count);
          const offsetCost = Math.max(0,convertEmissionsToOffsetCost(emissions) - amount_offset);

          const newData = [
            { label: "LLM calls", value: text_count },
            { label: "Image calls", value: image_count },
            { label: "Tons of CO2", value: emissions },
            { label: "Offset Cost", value: `$${offsetCost}` },
          ];

          setData(newData);
        } else {
          setIsNewUser(true)
        }
      } catch (err) {
        setError("Woah. Something happened");
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    setUserInfo();
  }, []);

  if (error) {
    redirect('/error');
  }

  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      tag: "",
    },
  })

  const onSubmit = async (value: z.infer<typeof formSchema>) => {
    setLoading(true);
  
    try {
      await addUser(value.tag);
      const newData = [
        { label: "LLM calls", value: 0 },
        { label: "Image calls", value: 0 },
        { label: "Tons of CO2", value: 0 },
        { label: "Offset Cost", value: '$0' },
      ];
      setData(newData);
    } catch (error) {
      console.error("Error adding user:", error);
      setError("Failed to add user.");
    } finally {
      setIsNewUser(false);
      setLoading(false);
    }
  };

  if (isNewUser) {
    return (
      <div className="text-white h-screen relative w-full overflow-hidden">
        <div className="absolute inset-0 -z-10">
          <video
            autoPlay
            muted
            loop
            playsInline
            className="w-full h-full object-cover"
            poster="https://cdn.prod.website-files.com/65b8cd72835ceeacd4449a53/65c8ed3e7d491e37259a30c5_Langchain-hero-1_1706794335%201-placeholder.jpg"
          >
            <source
              src="https://customer-xp1a3vy0ydc4ega7.cloudflarestream.com/bb6cf069546e3d829aa5808ac8b07748/downloads/default.mp4"
              type="video/mp4"
            />
            Your browser does not support the video tag.
          </video>
        </div>
{loading ? (
        <div className="pt-96 flex justify-center text-3xl">
          <MetroSpinner color="#fff" size={150} />
        </div>
      ) : (
        <div className="h-full flex flex-col items-center justify-center">
        <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 bg-white rounded-lg px-8 py-6 text-black w-96">
        <FormField
          control={form.control}
          name="tag"
          render={({ field }) => (
            <FormItem className="space-y-4">
              <FormLabel className="text-xl">Select a Tag</FormLabel>
              <FormControl>
                <Input placeholder="Enter a tag" className="text-lg" {...field} />
              </FormControl>
              <FormDescription className="text-md font-semibold">
                This is a public-facing tag used to identify your entity.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit" className="text-lg">Submit</Button>
      </form>
    </Form>
        </div>)}
      </div>)
  }

  return (
    <div className="text-white h-screen relative w-full overflow-hidden">
      <div className="absolute inset-0 -z-10">
        <video
          autoPlay
          muted
          loop
          playsInline
          className="w-full h-full object-cover"
          poster="https://cdn.prod.website-files.com/65b8cd72835ceeacd4449a53/65c8ed3e7d491e37259a30c5_Langchain-hero-1_1706794335%201-placeholder.jpg"
        >
          <source
            src="https://customer-xp1a3vy0ydc4ega7.cloudflarestream.com/bb6cf069546e3d829aa5808ac8b07748/downloads/default.mp4"
            type="video/mp4"
          />
          Your browser does not support the video tag.
        </video>
      </div>

      {loading ? (
        <div className="pt-96 flex justify-center text-3xl">
          <MetroSpinner color="#fff" size={150} />
        </div>
      ) : (
        <div className="pt-24">
          <SignedOut>
            <div className="flex justify-center text-3xl font-bold">
              <h1>This is what your dashboard will look like:</h1>
            </div>
          </SignedOut>

          <div className="pt-40 flex justify-evenly">
      
[truncated — 824 more characters]
```

### app/api/text/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { updateTextGenerationCalls } from "@/scripts/mongo";

export const dynamic = "force-dynamic";

export async function POST(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;

  try {
    const email = searchParams.get("email");

    if (!email) {
      throw new Error("Email is required");
    }

    const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

    if (!pattern.test(email)) {
      throw new Error("Invalid email input");
    }

    await updateTextGenerationCalls(email);

    return NextResponse.json({ message: "Successfully logged" }, { status: 200 });
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : "Unknown error";
    return NextResponse.json({ error: errorMessage }, { status: 400 });
  }
}

```

### app/api/image/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";
import { updateImageGenerationCalls } from "@/scripts/mongo";

export const dynamic = "force-dynamic";

export async function POST(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;

  try {
    const email = searchParams.get("email");

    if (!email) {
      throw new Error("Email is required");
    }

    const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

    if (!pattern.test(email)) {
      throw new Error("Invalid email input");
    }

    await updateImageGenerationCalls(email);

    return NextResponse.json({ message: "Successfully logged" }, { status: 200 });
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : "Unknown error";
    return NextResponse.json({ error: errorMessage }, { status: 400 });
  }
}

```

[21 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]